From eb0c4fd4d48f9a2237ec8a1b4c3f71c63cd5b794 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 18 Sep 2019 09:19:03 -0700 Subject: [PATCH 1/7] refactor parseQueryString --- awx/ui_next/src/util/qs.js | 466 +++++++++++++++----------------- awx/ui_next/src/util/qs.test.js | 108 +++++++- 2 files changed, 321 insertions(+), 253 deletions(-) diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index d10585aa99..cf182445c5 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -1,10 +1,226 @@ +/** + * Returns queryset config with defaults, if needed + * @param {string} namespace for appending to url querystring + * @param {object} default params that are not handled with search (page, page_size and order_by) + * @param {array} params that are number fields + * @return {object} query param object + */ +export function getQSConfig( + namespace, + defaultParams = { page: 1, page_size: 5, order_by: 'name' }, + integerFields = ['page', 'page_size'], + dateFields = ['modified', 'created'] +) { + if (!namespace) { + throw new Error('a QS namespace is required'); + } + return { + namespace, + defaultParams, + integerFields, + dateFields, + }; +} + +/** + * Convert url query string to query param object + * @param {object} qs config object (used for getting defaults, current query params etc.) + * @param {string} url query string + * @return {object} query param object + */ +// TODO: rename to parseNamespacedQueryString? +export function parseQueryString(config, queryString) { + if (!queryString) { + return config.defaultParams; + } + const params = stringToObject(config, queryString); + return addDefaultsToObject(config, params); +} + +/** + * Convert query param object to url query string + * Used to encode params for interacting with the api + * @param {object} qs config object for namespacing params, filtering defaults + * @param {object} query param object + * @return {string} url query string + */ +export const encodeQueryString = params => { + if (!params) return ''; + + return Object.keys(params) + .sort() + .filter(key => params[key] !== null) + .map(key => [key, params[key]]) + .map(([key, value]) => { + // if value is array, should return more than one key value pair + if (Array.isArray(value)) { + return value + .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) + .join('&'); + } + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + }) + .join('&'); +}; + +/** + * Convert query param object to url query string, adding namespace and removing defaults + * Used to put into url bar after ui route + * @param {object} qs config object for namespacing params, filtering defaults + * @param {object} query param object + * @return {string} url query string + */ +export const encodeNonDefaultQueryString = (config, params) => { + if (!params) return ''; + + const namespacedParams = namespaceParams(config.namespace, params); + const namespacedDefaults = namespaceParams( + config.namespace, + config.defaultParams + ); + const namespacedDefaultKeys = Object.keys(namespacedDefaults); + const namespacedParamsWithoutDefaultsKeys = Object.keys( + namespacedParams + ).filter( + key => + namespacedDefaultKeys.indexOf(key) === -1 || + !paramValueIsEqual(namespacedParams[key], namespacedDefaults[key]) + ); + + return namespacedParamsWithoutDefaultsKeys + .sort() + .filter(key => namespacedParams[key] !== null) + .map(key => { + return [key, namespacedParams[key]]; + }) + .map(([key, value]) => { + // if value is array, should return more than one key value pair + if (Array.isArray(value)) { + return value + .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) + .join('&'); + } + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + }) + .join('&'); +}; + +/** + * Merges existing params of search string with new ones and returns the updated list of params + * @param {object} qs config object (used for getting defaults, current query params etc.) + * @param {object} object with params from existing search + * @param {object} object with new params to add + * @return {object} query param object + */ +export function addParams(config, oldParams, paramsToAdd) { + const namespacedOldParams = namespaceParams(config.namespace, oldParams); + const namespacedParamsToAdd = namespaceParams(config.namespace, paramsToAdd); + const namespacedDefaultParams = namespaceParams( + config.namespace, + config.defaultParams + ); + + const namespacedOldParamsNotDefaults = getNonDefaultParams( + namespacedOldParams, + namespacedDefaultParams + ); + const namespacedMergedParams = getMergedParams( + namespacedOldParamsNotDefaults, + namespacedParamsToAdd + ); + + // return updated params. + // If newParams includes updates to the defaults, they will be replaced, + // not concatenated. + return denamespaceParams(config.namespace, { + ...getDefaultParams(namespacedOldParams, namespacedDefaultParams), + ...namespacedMergedParams, + ...getRemainingNewParams(namespacedMergedParams, namespacedParamsToAdd), + }); +} + +/** + * Removes params from the search string and returns the updated list of params + * @param {object} qs config object (used for getting defaults, current query params etc.) + * @param {object} object with params from existing search + * @param {object} object with new params to remove + * @return {object} query param object + */ +export function removeParams(config, oldParams, paramsToRemove) { + const paramsEntries = []; + Object.entries(oldParams).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(val => { + paramsEntries.push([key, val]); + }); + } else { + paramsEntries.push([key, value]); + } + }); + const paramsToRemoveEntries = Object.entries(paramsToRemove); + const remainingEntries = paramsEntries.filter( + ([key, value]) => + paramsToRemoveEntries.filter( + ([newKey, newValue]) => key === newKey && value === newValue + ).length === 0 + ); + const remainingObject = arrayToObject(remainingEntries); + const defaultEntriesLeftover = Object.entries(config.defaultParams).filter( + ([key]) => !remainingObject[key] + ); + const finalParamsEntries = remainingEntries; + defaultEntriesLeftover.forEach(value => { + finalParamsEntries.push(value); + }); + return arrayToObject(finalParamsEntries); +} + +const stringToObject = (config, qs) => { + const params = {}; + qs.replace(/^\?/, '') + .split('&') + .map(s => s.split('=')) + .forEach(([nsKey, rawValue]) => { + if (!nsKey || !nsKey.startsWith(`${config.namespace}.`)) { + return; + } + const key = decodeURIComponent(nsKey.substr(config.namespace.length + 1)); + const value = parseValue(config, key, rawValue); + if (!params[key]) { + params[key] = value; + } else if (Array.isArray(params[key])) { + params[key].push(value); + } else { + params[key] = [params[key], value]; + } + }); + return params; +}; +export { stringToObject as _stringToObject }; + +function addDefaultsToObject(config, params) { + return { + ...config.defaultParams, + ...params, + }; +} +export { addDefaultsToObject as _addDefaultsToObject }; + +function parseValue(config, key, rawValue) { + if (config.integerFields && config.integerFields.some(v => v === key)) { + return parseInt(rawValue, 10); + } + // TODO: parse date fields + return decodeURIComponent(rawValue); +} + /** * helper function used to convert from * Object.entries format ([ [ key, value ], ... ]) to object * @param {array} array in the format [ [ key, value ], ...] * @return {object} object in the forms { key: value, ... } */ -const toObject = entriesArr => +const arrayToObject = entriesArr => entriesArr.reduce((acc, [key, value]) => { if (acc[key] && Array.isArray(acc[key])) { acc[key].push(value); @@ -83,176 +299,6 @@ const paramValueIsEqual = (one, two) => { return isEqual; }; -/** - * Convert query param object to url query string - * Used to encode params for interacting with the api - * @param {object} qs config object for namespacing params, filtering defaults - * @param {object} query param object - * @return {string} url query string - */ -export const encodeQueryString = params => { - if (!params) return ''; - - return Object.keys(params) - .sort() - .filter(key => params[key] !== null) - .map(key => [key, params[key]]) - .map(([key, value]) => { - // if value is array, should return more than one key value pair - if (Array.isArray(value)) { - return value - .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) - .join('&'); - } - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }) - .join('&'); -}; - -/** - * Convert query param object to url query string, adding namespace and removing defaults - * Used to put into url bar after ui route - * @param {object} qs config object for namespacing params, filtering defaults - * @param {object} query param object - * @return {string} url query string - */ -export const encodeNonDefaultQueryString = (config, params) => { - if (!params) return ''; - - const namespacedParams = namespaceParams(config.namespace, params); - const namespacedDefaults = namespaceParams( - config.namespace, - config.defaultParams - ); - const namespacedDefaultKeys = Object.keys(namespacedDefaults); - const namespacedParamsWithoutDefaultsKeys = Object.keys( - namespacedParams - ).filter( - key => - namespacedDefaultKeys.indexOf(key) === -1 || - !paramValueIsEqual(namespacedParams[key], namespacedDefaults[key]) - ); - - return namespacedParamsWithoutDefaultsKeys - .sort() - .filter(key => namespacedParams[key] !== null) - .map(key => { - return [key, namespacedParams[key]]; - }) - .map(([key, value]) => { - // if value is array, should return more than one key value pair - if (Array.isArray(value)) { - return value - .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) - .join('&'); - } - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }) - .join('&'); -}; - -/** - * Returns queryset config with defaults, if needed - * @param {string} namespace for appending to url querystring - * @param {object} default params that are not handled with search (page, page_size and order_by) - * @param {array} params that are number fields - * @return {object} query param object - */ -export function getQSConfig( - namespace, - defaultParams = { page: 1, page_size: 5, order_by: 'name' }, - integerFields = ['page', 'page_size'], - dateFields = ['modified', 'created'] -) { - if (!namespace) { - throw new Error('a QS namespace is required'); - } - return { - namespace, - defaultParams, - integerFields, - dateFields, - }; -} - -/** - * Convert url query string to query param object - * @param {object} qs config object (used for getting defaults, current query params etc.) - * @param {string} url query string - * @return {object} query param object - */ -export function parseQueryString(config, queryString) { - if (!queryString) return config.defaultParams; - - const namespacedIntegerFields = config.integerFields.map(f => - config.namespace ? `${config.namespace}.${f}` : f - ); - - const keyValuePairs = queryString - .replace(/^\?/, '') - .split('&') - .map(s => s.split('=')) - .map(([key, value]) => { - if (namespacedIntegerFields.includes(key)) { - return [decodeURIComponent(key), parseInt(value, 10)]; - } - - return [decodeURIComponent(key), decodeURIComponent(value)]; - }); - - const keyValueObject = toObject(keyValuePairs); - - // needs to return array for duplicate keys - // ie [[k1, v1], [k1, v2], [k2, v3]] - // -> [[k1, [v1, v2]], [k2, v3]] - const dedupedKeyValuePairs = Object.keys(keyValueObject).map(key => { - const values = keyValuePairs.filter(([k]) => k === key).map(([, v]) => v); - - if (values.length === 1) { - return [key, values[0]]; - } - - return [key, values]; - }); - - const parsed = Object.assign( - ...dedupedKeyValuePairs.map(([k, v]) => ({ - [k]: v, - })) - ); - - const namespacedParams = {}; - - Object.keys(parsed).forEach(field => { - if (namespaceMatches(config.namespace, field)) { - let fieldname = field; - if (config.namespace) { - fieldname = field.substr(config.namespace.length + 1); - } - namespacedParams[fieldname] = parsed[field]; - } - }); - - const namespacedDefaults = namespaceParams( - config.namespace, - config.defaultParams - ); - - Object.keys(namespacedDefaults) - .filter(key => Object.keys(parsed).indexOf(key) === -1) - .forEach(field => { - if (namespaceMatches(config.namespace, field)) { - let fieldname = field; - if (config.namespace) { - fieldname = field.substr(config.namespace.length + 1); - } - namespacedParams[fieldname] = namespacedDefaults[field]; - } - }); - - return namespacedParams; -} - /** * helper function to get params that are defaults * @param {object} namespaced params object @@ -260,7 +306,7 @@ export function parseQueryString(config, queryString) { * @return {object} namespaced params object of only defaults */ const getDefaultParams = (params, defaults) => - toObject( + arrayToObject( Object.keys(params) .filter(key => Object.keys(defaults).indexOf(key) > -1) .map(key => [key, params[key]]) @@ -273,7 +319,7 @@ const getDefaultParams = (params, defaults) => * @return {object} namespaced params object of non-defaults */ const getNonDefaultParams = (params, defaults) => - toObject( + arrayToObject( Object.keys(params) .filter(key => Object.keys(defaults).indexOf(key) === -1) .map(key => [key, params[key]]) @@ -286,7 +332,7 @@ const getNonDefaultParams = (params, defaults) => * @return {object} merged namespaced params object */ const getMergedParams = (oldParams, newParams) => - toObject( + arrayToObject( Object.keys(oldParams).map(key => { let oldVal = oldParams[key]; const newVal = newParams[key]; @@ -308,78 +354,8 @@ const getMergedParams = (oldParams, newParams) => * @return {object} remaining new namespaced params object */ const getRemainingNewParams = (mergedParams, newParams) => - toObject( + arrayToObject( Object.keys(newParams) .filter(key => Object.keys(mergedParams).indexOf(key) === -1) .map(key => [key, newParams[key]]) ); - -/** - * Merges existing params of search string with new ones and returns the updated list of params - * @param {object} qs config object (used for getting defaults, current query params etc.) - * @param {object} object with params from existing search - * @param {object} object with new params to add - * @return {object} query param object - */ -export function addParams(config, oldParams, paramsToAdd) { - const namespacedOldParams = namespaceParams(config.namespace, oldParams); - const namespacedParamsToAdd = namespaceParams(config.namespace, paramsToAdd); - const namespacedDefaultParams = namespaceParams( - config.namespace, - config.defaultParams - ); - - const namespacedOldParamsNotDefaults = getNonDefaultParams( - namespacedOldParams, - namespacedDefaultParams - ); - const namespacedMergedParams = getMergedParams( - namespacedOldParamsNotDefaults, - namespacedParamsToAdd - ); - - // return updated params. - // If newParams includes updates to the defaults, they will be replaced, - // not concatenated. - return denamespaceParams(config.namespace, { - ...getDefaultParams(namespacedOldParams, namespacedDefaultParams), - ...namespacedMergedParams, - ...getRemainingNewParams(namespacedMergedParams, namespacedParamsToAdd), - }); -} - -/** - * Removes params from the search string and returns the updated list of params - * @param {object} qs config object (used for getting defaults, current query params etc.) - * @param {object} object with params from existing search - * @param {object} object with new params to remove - * @return {object} query param object - */ -export function removeParams(config, oldParams, paramsToRemove) { - const paramsEntries = []; - Object.entries(oldParams).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(val => { - paramsEntries.push([key, val]); - }); - } else { - paramsEntries.push([key, value]); - } - }); - const paramsToRemoveEntries = Object.entries(paramsToRemove); - const remainingEntries = paramsEntries.filter( - ([key, value]) => - paramsToRemoveEntries.filter( - ([newKey, newValue]) => key === newKey && value === newValue - ).length === 0 - ); - const remainingObject = toObject(remainingEntries); - const defaultEntriesLeftover = Object.entries(config.defaultParams).filter( - ([key]) => !remainingObject[key] - ); - const finalParamsEntries = remainingEntries; - defaultEntriesLeftover.forEach(value => { - finalParamsEntries.push(value); - }); - return toObject(finalParamsEntries); -} diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index b3c459b9ba..3ddd3a94c4 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -5,6 +5,9 @@ import { getQSConfig, addParams, removeParams, + _paramStringToArray, + _stringToObject, + _addDefaultsToObject, } from './qs'; describe('qs (qs.js)', () => { @@ -81,6 +84,7 @@ describe('qs (qs.js)', () => { dateFields: ['modified', 'created'], defaultParams: { page: 1, page_size: 5, order_by: 'name' }, integerFields: ['page', 'page_size'], + dateFields: ['modified', 'created'], }); }); @@ -98,6 +102,7 @@ describe('qs (qs.js)', () => { dateFields: ['modified', 'created'], defaultParams: { page: 1, page_size: 15 }, integerFields: ['page', 'page_size'], + dateFields: ['modified', 'created'], }); }); }); @@ -105,11 +110,11 @@ describe('qs (qs.js)', () => { describe('parseQueryString', () => { test('should get query params', () => { const config = { - namespace: null, + namespace: 'item', defaultParams: { page: 1, page_size: 15 }, integerFields: ['page', 'page_size'], }; - const query = '?baz=bar&page=3'; + const query = '?item.baz=bar&item.page=3'; expect(parseQueryString(config, query)).toEqual({ baz: 'bar', page: 3, @@ -132,11 +137,11 @@ describe('qs (qs.js)', () => { test('should get query params with correct integer fields', () => { const config = { - namespace: null, + namespace: 'item', defaultParams: {}, integerFields: ['page', 'foo'], }; - const query = '?foo=4&bar=5'; + const query = '?item.foo=4&item.bar=5'; expect(parseQueryString(config, query)).toEqual({ foo: 4, bar: '5', @@ -145,16 +150,28 @@ describe('qs (qs.js)', () => { test('should decode parsed params', () => { const config = { - namespace: null, + namespace: 'item', defaultParams: {}, integerFields: ['page'], }; - const query = '?foo=bar%20baz'; + const query = '?item.foo=bar%20baz'; expect(parseQueryString(config, query)).toEqual({ foo: 'bar baz', }); }); + test('should decode param keys', () => { + const config = { + namespace: 'item', + defaultParams: {}, + integerFields: ['page'], + }; + const query = '?item.foo%20bar=baz'; + expect(parseQueryString(config, query)).toEqual({ + 'foo bar': 'baz', + }); + }); + test('should get namespaced query params', () => { const config = { namespace: 'inventory', @@ -199,11 +216,11 @@ describe('qs (qs.js)', () => { test('should add duplicate non-default params as array', () => { const config = { - namespace: null, + namespace: 'item', defaultParams: { page: 1, page_size: 15 }, integerFields: ['page', 'page_size'], }; - const query = '?baz=bar&baz=boo&page=3'; + const query = '?item.baz=bar&item.baz=boo&item.page=3'; expect(parseQueryString(config, query)).toEqual({ baz: ['bar', 'boo'], page: 3, @@ -550,4 +567,79 @@ describe('qs (qs.js)', () => { }); }); }); + + describe('_stringToObject', () => { + it('should convert to object', () => { + const config = { namespace: 'unit' }; + expect(_stringToObject(config, '?unit.foo=bar&unit.baz=bam')).toEqual({ + foo: 'bar', + baz: 'bam', + }); + }); + + it('should convert duplicated keys to array', () => { + const config = { namespace: 'unit' }; + expect(_stringToObject(config, '?unit.foo=bar&unit.foo=bam')).toEqual({ + foo: ['bar', 'bam'], + }); + }); + + it('should omit keys from other namespaces', () => { + const config = { namespace: 'unit' }; + expect( + _stringToObject(config, '?unit.foo=bar&other.bar=bam&one=two') + ).toEqual({ + foo: 'bar', + }); + }); + + it('should convert numbers to correct type', () => { + const config = { + namespace: 'unit', + integerFields: ['page'], + }; + expect(_stringToObject(config, '?unit.page=3')).toEqual({ + page: 3, + }); + }); + }); + + describe('_addDefaultsToObject', () => { + it('should add missing default values', () => { + const config = { + defaultParams: { page: 1, page_size: 5, order_by: 'name' }, + } + expect(_addDefaultsToObject(config, {})).toEqual({ + page: 1, + page_size: 5, + order_by: 'name', + }); + }); + + it('should not override existing params', () => { + const config = { + defaultParams: { page: 1, page_size: 5, order_by: 'name' }, + } + const params = { + page: 2, + order_by: 'date_created', + } + expect(_addDefaultsToObject(config, params)).toEqual({ + page: 2, + page_size: 5, + order_by: 'date_created', + }); + }); + + it('should handle missing defaultParams', () => { + const params = { + page: 2, + order_by: 'date_created', + } + expect(_addDefaultsToObject({}, params)).toEqual({ + page: 2, + order_by: 'date_created', + }); + }) + }) }); From d6f93737c4631a3f0700250f313d4a7f5315a828 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 18 Sep 2019 11:29:51 -0700 Subject: [PATCH 2/7] improve qs test coverage, minor cleanup --- awx/ui_next/src/util/qs.js | 55 +++++++++++++----------------- awx/ui_next/src/util/qs.test.js | 60 ++++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 39 deletions(-) diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index cf182445c5..741bd6c10d 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -28,7 +28,6 @@ export function getQSConfig( * @param {string} url query string * @return {object} query param object */ -// TODO: rename to parseNamespacedQueryString? export function parseQueryString(config, queryString) { if (!queryString) { return config.defaultParams; @@ -51,18 +50,19 @@ export const encodeQueryString = params => { .sort() .filter(key => params[key] !== null) .map(key => [key, params[key]]) - .map(([key, value]) => { - // if value is array, should return more than one key value pair - if (Array.isArray(value)) { - return value - .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) - .join('&'); - } - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }) + .map(([key, value]) => encodeValue(key, value)) .join('&'); }; +function encodeValue(key, value) { + if (Array.isArray(value)) { + return value + .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) + .join('&'); + } + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; +} + /** * Convert query param object to url query string, adding namespace and removing defaults * Used to put into url bar after ui route @@ -93,22 +93,15 @@ export const encodeNonDefaultQueryString = (config, params) => { .map(key => { return [key, namespacedParams[key]]; }) - .map(([key, value]) => { - // if value is array, should return more than one key value pair - if (Array.isArray(value)) { - return value - .map(val => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`) - .join('&'); - } - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - }) + .map(([key, value]) => encodeValue(key, value)) .join('&'); }; /** - * Merges existing params of search string with new ones and returns the updated list of params + * Merges existing params of query string with new ones and returns the + * updated list of params * @param {object} qs config object (used for getting defaults, current query params etc.) - * @param {object} object with params from existing search + * @param {object} object with params from existing query * @param {object} object with new params to add * @return {object} query param object */ @@ -181,7 +174,7 @@ const stringToObject = (config, qs) => { .split('&') .map(s => s.split('=')) .forEach(([nsKey, rawValue]) => { - if (!nsKey || !nsKey.startsWith(`${config.namespace}.`)) { + if (!nsKey || !namespaceMatches(config.namespace, nsKey)) { return; } const key = decodeURIComponent(nsKey.substr(config.namespace.length + 1)); @@ -198,14 +191,6 @@ const stringToObject = (config, qs) => { }; export { stringToObject as _stringToObject }; -function addDefaultsToObject(config, params) { - return { - ...config.defaultParams, - ...params, - }; -} -export { addDefaultsToObject as _addDefaultsToObject }; - function parseValue(config, key, rawValue) { if (config.integerFields && config.integerFields.some(v => v === key)) { return parseInt(rawValue, 10); @@ -214,6 +199,14 @@ function parseValue(config, key, rawValue) { return decodeURIComponent(rawValue); } +function addDefaultsToObject(config, params) { + return { + ...config.defaultParams, + ...params, + }; +} +export { addDefaultsToObject as _addDefaultsToObject }; + /** * helper function used to convert from * Object.entries format ([ [ key, value ], ... ]) to object @@ -288,7 +281,7 @@ const paramValueIsEqual = (one, two) => { let isEqual = false; if (Array.isArray(one) && Array.isArray(two)) { - isEqual = one.filter(val => two.indexOf(val) > -1).length === 0; + isEqual = one.filter(val => two.indexOf(val) > -1).length === one.length; } else if ( (typeof one === 'string' && typeof two === 'string') || (typeof one === 'number' && typeof two === 'number') diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index 3ddd3a94c4..c3599d5681 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -5,7 +5,6 @@ import { getQSConfig, addParams, removeParams, - _paramStringToArray, _stringToObject, _addDefaultsToObject, } from './qs'; @@ -38,6 +37,13 @@ describe('qs (qs.js)', () => { }; expect(encodeQueryString(vals)).toEqual('order_by=name'); }); + + test('should encode array params', () => { + const vals = { + foo: ['one', 'two', 'three'], + }; + expect(encodeQueryString(vals)).toEqual('foo=one&foo=two&foo=three'); + }); }); describe('encodeNonDefaultQueryString', () => { @@ -75,6 +81,18 @@ describe('qs (qs.js)', () => { }; expect(encodeNonDefaultQueryString(config, vals)).toEqual('order_by=foo'); }); + + test('should compare array values', () => { + const vals = { + foo: ['one', 'two'], + }; + const conf = { + defaultParams: { + foo: ['one', 'two'], + } + }; + expect(encodeNonDefaultQueryString(conf, vals)).toEqual(''); + }) }); describe('getQSConfig', () => { @@ -241,6 +259,16 @@ describe('qs (qs.js)', () => { page_size: 15, }); }); + + test('should parse long arrays', () => { + const config = { + namespace: 'item', + }; + const query = '?item.baz=one&item.baz=two&item.baz=three'; + expect(parseQueryString(config, query)).toEqual({ + baz: ['one', 'two', 'three'], + }); + }); }); describe('addParams', () => { @@ -306,6 +334,22 @@ describe('qs (qs.js)', () => { }); }); + test('should convert param to array when merging', () => { + const config = { + namespace: null, + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['page', 'page_size'], + }; + const oldParams = { baz: 'bar', page: 3, page_size: 15 }; + const newParams = { baz: 'bust', pat: 'pal' }; + expect(addParams(config, oldParams, newParams)).toEqual({ + baz: ['bar', 'bust'], + pat: 'pal', + page: 3, + page_size: 15, + }); + }); + test('should add namespaced query params', () => { const config = { namespace: 'item', @@ -569,7 +613,7 @@ describe('qs (qs.js)', () => { }); describe('_stringToObject', () => { - it('should convert to object', () => { + test('should convert to object', () => { const config = { namespace: 'unit' }; expect(_stringToObject(config, '?unit.foo=bar&unit.baz=bam')).toEqual({ foo: 'bar', @@ -577,14 +621,14 @@ describe('qs (qs.js)', () => { }); }); - it('should convert duplicated keys to array', () => { + test('should convert duplicated keys to array', () => { const config = { namespace: 'unit' }; expect(_stringToObject(config, '?unit.foo=bar&unit.foo=bam')).toEqual({ foo: ['bar', 'bam'], }); }); - it('should omit keys from other namespaces', () => { + test('should omit keys from other namespaces', () => { const config = { namespace: 'unit' }; expect( _stringToObject(config, '?unit.foo=bar&other.bar=bam&one=two') @@ -593,7 +637,7 @@ describe('qs (qs.js)', () => { }); }); - it('should convert numbers to correct type', () => { + test('should convert numbers to correct type', () => { const config = { namespace: 'unit', integerFields: ['page'], @@ -605,7 +649,7 @@ describe('qs (qs.js)', () => { }); describe('_addDefaultsToObject', () => { - it('should add missing default values', () => { + test('should add missing default values', () => { const config = { defaultParams: { page: 1, page_size: 5, order_by: 'name' }, } @@ -616,7 +660,7 @@ describe('qs (qs.js)', () => { }); }); - it('should not override existing params', () => { + test('should not override existing params', () => { const config = { defaultParams: { page: 1, page_size: 5, order_by: 'name' }, } @@ -631,7 +675,7 @@ describe('qs (qs.js)', () => { }); }); - it('should handle missing defaultParams', () => { + test('should handle missing defaultParams', () => { const params = { page: 2, order_by: 'date_created', From 3ea4a32940514f3ede6c56e77024d77739a27d7e Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Sep 2019 08:48:44 -0700 Subject: [PATCH 3/7] qs cleanup, more tests --- awx/ui_next/src/util/qs.js | 62 ++++++++++++++------- awx/ui_next/src/util/qs.test.js | 98 ++++++++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 34 deletions(-) diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index 741bd6c10d..1a169399e9 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -64,8 +64,8 @@ function encodeValue(key, value) { } /** - * Convert query param object to url query string, adding namespace and removing defaults - * Used to put into url bar after ui route + * Convert query param object to url query string, adding namespace and + * removing defaults. Used to put into url bar after ui route * @param {object} qs config object for namespacing params, filtering defaults * @param {object} query param object * @return {string} url query string @@ -106,6 +106,16 @@ export const encodeNonDefaultQueryString = (config, params) => { * @return {object} query param object */ export function addParams(config, oldParams, paramsToAdd) { + // const oldParamsMinusDefaults = getNonDefaultParams(oldParams, config.defaultParams); + // const merged = mergeParams(oldParamsMinusDefaults, paramsToAdd); + // console.log(oldParamsMinusDefaults, merged); + // + // return { + // ...config.defaultParams, + // ...merged, + // ...getRemainingNewParams(merged, paramsToAdd), + // }; + const namespacedOldParams = namespaceParams(config.namespace, oldParams); const namespacedParamsToAdd = namespaceParams(config.namespace, paramsToAdd); const namespacedDefaultParams = namespaceParams( @@ -117,7 +127,7 @@ export function addParams(config, oldParams, paramsToAdd) { namespacedOldParams, namespacedDefaultParams ); - const namespacedMergedParams = getMergedParams( + const namespacedMergedParams = mergeParams( namespacedOldParamsNotDefaults, namespacedParamsToAdd ); @@ -179,13 +189,7 @@ const stringToObject = (config, qs) => { } const key = decodeURIComponent(nsKey.substr(config.namespace.length + 1)); const value = parseValue(config, key, rawValue); - if (!params[key]) { - params[key] = value; - } else if (Array.isArray(params[key])) { - params[key].push(value); - } else { - params[key] = [params[key], value]; - } + params[key] = mergeParam(params[key], value); }); return params; }; @@ -324,21 +328,37 @@ const getNonDefaultParams = (params, defaults) => * @param {object} namespaced params object of new params * @return {object} merged namespaced params object */ -const getMergedParams = (oldParams, newParams) => - arrayToObject( +// TODO: BUG? newParam that doesn't exist in oldParams isn't added(?) +const mergeParams = (oldParams, newParams) => { + const merged = arrayToObject( Object.keys(oldParams).map(key => { - let oldVal = oldParams[key]; + const oldVal = oldParams[key]; const newVal = newParams[key]; - if (newVal) { - if (Array.isArray(oldVal)) { - oldVal.push(newVal); - } else { - oldVal = [oldVal, newVal]; - } - } - return [key, oldVal]; + return [key, mergeParam(oldVal, newVal)]; }) ); + Object.keys(newParams).forEach(key => { + if (!merged[key]) { + merged[key] = newParams[key]; + } + }); + return merged; +} +export { mergeParams as _mergeParams }; + +function mergeParam(oldVal, newVal) { + if (!newVal) { + return oldVal; + } + if (!oldVal) { + return newVal; + } + if (Array.isArray(oldVal)) { + oldVal.push(newVal); + return oldVal; + } + return [oldVal, newVal]; +} /** * helper function to get new params that are not in merged params diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index c3599d5681..d97ebcffd8 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -7,6 +7,7 @@ import { removeParams, _stringToObject, _addDefaultsToObject, + _mergeParams, } from './qs'; describe('qs (qs.js)', () => { @@ -53,7 +54,7 @@ describe('qs (qs.js)', () => { integerFields: ['page'], }; - test('encodeNonDefaultQueryString returns the expected queryString', () => { + test('should return the expected queryString', () => { [ [null, ''], [{}, ''], @@ -74,7 +75,7 @@ describe('qs (qs.js)', () => { }); }); - test('encodeNonDefaultQueryString omits null values', () => { + test('should omit null values', () => { const vals = { order_by: 'foo', page: null, @@ -82,17 +83,32 @@ describe('qs (qs.js)', () => { expect(encodeNonDefaultQueryString(config, vals)).toEqual('order_by=foo'); }); - test('should compare array values', () => { + test('should namespace encoded params', () => { + const conf = { + namespace: 'item', + defaultParams: { page: 1 }, + }; + const params = { + page: 1, + foo: 'bar', + } + expect(encodeNonDefaultQueryString(conf, params)).toEqual('item.foo=bar'); + }); + + test('should handle array values', () => { const vals = { foo: ['one', 'two'], + bar: ['alpha', 'beta'], }; const conf = { defaultParams: { foo: ['one', 'two'], - } + }, }; - expect(encodeNonDefaultQueryString(conf, vals)).toEqual(''); - }) + expect(encodeNonDefaultQueryString(conf, vals)).toEqual( + 'bar=alpha&bar=beta' + ); + }); }); describe('getQSConfig', () => { @@ -303,7 +319,7 @@ describe('qs (qs.js)', () => { }); }); - test('should replace query params that are defaults', () => { + test('should replace query params that have defaults', () => { const config = { namespace: null, defaultParams: { page: 1, page_size: 15 }, @@ -318,6 +334,21 @@ describe('qs (qs.js)', () => { }); }); + test('should replace query params that match defaults', () => { + const config = { + namespace: null, + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['page', 'page_size'], + }; + const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; + const newParams = { page_size: 5 }; + expect(addParams(config, oldParams, newParams)).toEqual({ + baz: ['bar', 'bang'], + page: 3, + page_size: 5, + }); + }); + test('should add multiple params', () => { const config = { namespace: null, @@ -652,7 +683,7 @@ describe('qs (qs.js)', () => { test('should add missing default values', () => { const config = { defaultParams: { page: 1, page_size: 5, order_by: 'name' }, - } + }; expect(_addDefaultsToObject(config, {})).toEqual({ page: 1, page_size: 5, @@ -663,11 +694,11 @@ describe('qs (qs.js)', () => { test('should not override existing params', () => { const config = { defaultParams: { page: 1, page_size: 5, order_by: 'name' }, - } + }; const params = { page: 2, order_by: 'date_created', - } + }; expect(_addDefaultsToObject(config, params)).toEqual({ page: 2, page_size: 5, @@ -679,11 +710,52 @@ describe('qs (qs.js)', () => { const params = { page: 2, order_by: 'date_created', - } + }; expect(_addDefaultsToObject({}, params)).toEqual({ page: 2, order_by: 'date_created', }); - }) - }) + }); + }); + + describe('mergeParams', () => { + it('should merge param into an array', () => { + const oldParams = { + foo: 'one', + }; + const newParams = { + foo: 'two', + }; + expect(_mergeParams(oldParams, newParams)).toEqual({ + foo: ['one', 'two'], + }); + }); + + it('should retain unaltered params', () => { + const oldParams = { + foo: 'one', + bar: 'baz' + }; + const newParams = { + foo: 'two', + }; + expect(_mergeParams(oldParams, newParams)).toEqual({ + foo: ['one', 'two'], + bar: 'baz', + }); + }); + + it('should merge objects', () => { + const oldParams = { + one: 'one', + }; + const newParams = { + two: 'two', + }; + expect(_mergeParams(oldParams, newParams)).toEqual({ + one: 'one', + two: 'two', + }); + }); + }); }); From cb69cac62d67afe2d7a407651675b24b2e11a9a2 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Sep 2019 10:52:18 -0700 Subject: [PATCH 4/7] split qs addParams into mergeParams and replaceParams for clarity --- .../src/components/ListHeader/ListHeader.jsx | 7 +- .../PaginatedDataList/PaginatedDataList.jsx | 8 +- awx/ui_next/src/util/qs.js | 144 ++------- awx/ui_next/src/util/qs.test.js | 297 +++++++----------- 4 files changed, 155 insertions(+), 301 deletions(-) diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.jsx index 4974c001c9..92dcad3a3e 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.jsx @@ -9,7 +9,8 @@ import FilterTags from '@components/FilterTags'; import { encodeNonDefaultQueryString, parseQueryString, - addParams, + mergeParams, + replaceParams, removeParams, } from '@util/qs'; import { QSConfig } from '@types'; @@ -48,7 +49,7 @@ class ListHeader extends React.Component { const { history, qsConfig } = this.props; const { search } = history.location; const oldParams = parseQueryString(qsConfig, search); - this.pushHistoryState(addParams(qsConfig, oldParams, { [key]: value })); + this.pushHistoryState(mergeParams(oldParams, { [key]: value })); } handleRemove(key, value) { @@ -67,7 +68,7 @@ class ListHeader extends React.Component { const { search } = history.location; const oldParams = parseQueryString(qsConfig, search); this.pushHistoryState( - addParams(qsConfig, oldParams, { + replaceParams(oldParams, { order_by: order === 'ascending' ? key : `-${key}`, page: null, }) diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx index d238595212..12751eacdf 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx @@ -15,7 +15,7 @@ import DataListToolbar from '@components/DataListToolbar'; import { encodeNonDefaultQueryString, parseQueryString, - addParams, + replaceParams, } from '@util/qs'; import { pluralize, ucFirst } from '@util/strings'; @@ -34,16 +34,14 @@ class PaginatedDataList extends React.Component { const { history, qsConfig } = this.props; const { search } = history.location; const oldParams = parseQueryString(qsConfig, search); - this.pushHistoryState(addParams(qsConfig, oldParams, { page: pageNumber })); + this.pushHistoryState(replaceParams(oldParams, { page: pageNumber })); } handleSetPageSize(event, pageSize) { const { history, qsConfig } = this.props; const { search } = history.location; const oldParams = parseQueryString(qsConfig, search); - this.pushHistoryState( - addParams(qsConfig, oldParams, { page_size: pageSize }) - ); + this.pushHistoryState(replaceParams(oldParams, { page_size: pageSize })); } pushHistoryState(params) { diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index 1a169399e9..75df652120 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -97,51 +97,6 @@ export const encodeNonDefaultQueryString = (config, params) => { .join('&'); }; -/** - * Merges existing params of query string with new ones and returns the - * updated list of params - * @param {object} qs config object (used for getting defaults, current query params etc.) - * @param {object} object with params from existing query - * @param {object} object with new params to add - * @return {object} query param object - */ -export function addParams(config, oldParams, paramsToAdd) { - // const oldParamsMinusDefaults = getNonDefaultParams(oldParams, config.defaultParams); - // const merged = mergeParams(oldParamsMinusDefaults, paramsToAdd); - // console.log(oldParamsMinusDefaults, merged); - // - // return { - // ...config.defaultParams, - // ...merged, - // ...getRemainingNewParams(merged, paramsToAdd), - // }; - - const namespacedOldParams = namespaceParams(config.namespace, oldParams); - const namespacedParamsToAdd = namespaceParams(config.namespace, paramsToAdd); - const namespacedDefaultParams = namespaceParams( - config.namespace, - config.defaultParams - ); - - const namespacedOldParamsNotDefaults = getNonDefaultParams( - namespacedOldParams, - namespacedDefaultParams - ); - const namespacedMergedParams = mergeParams( - namespacedOldParamsNotDefaults, - namespacedParamsToAdd - ); - - // return updated params. - // If newParams includes updates to the defaults, they will be replaced, - // not concatenated. - return denamespaceParams(config.namespace, { - ...getDefaultParams(namespacedOldParams, namespacedDefaultParams), - ...namespacedMergedParams, - ...getRemainingNewParams(namespacedMergedParams, namespacedParamsToAdd), - }); -} - /** * Removes params from the search string and returns the updated list of params * @param {object} qs config object (used for getting defaults, current query params etc.) @@ -199,7 +154,7 @@ function parseValue(config, key, rawValue) { if (config.integerFields && config.integerFields.some(v => v === key)) { return parseInt(rawValue, 10); } - // TODO: parse date fields + // TODO: parse dateFields into date format? return decodeURIComponent(rawValue); } @@ -246,23 +201,6 @@ const namespaceParams = (namespace, params = {}) => { return namespaced || {}; }; -/** - * helper function to remove namespace from params object - * @param {string} namespace to remove from params - * @param {object} params object to append namespace to - * @return {object} params object with non-namespaced keys - */ -const denamespaceParams = (namespace, params = {}) => { - if (!namespace) return params; - - const denamespaced = {}; - Object.keys(params).forEach(key => { - denamespaced[key.substr(namespace.length + 1)] = params[key]; - }); - - return denamespaced; -}; - /** * helper function to check the namespace of a param is what you expec * @param {string} namespace to append to params @@ -297,46 +235,16 @@ const paramValueIsEqual = (one, two) => { }; /** - * helper function to get params that are defaults - * @param {object} namespaced params object - * @param {object} namespaced params object of default params - * @return {object} namespaced params object of only defaults - */ -const getDefaultParams = (params, defaults) => - arrayToObject( - Object.keys(params) - .filter(key => Object.keys(defaults).indexOf(key) > -1) - .map(key => [key, params[key]]) - ); - -/** - * helper function to get params that are not defaults - * @param {object} namespaced params object - * @param {object} namespaced params object of default params - * @return {object} namespaced params object of non-defaults - */ -const getNonDefaultParams = (params, defaults) => - arrayToObject( - Object.keys(params) - .filter(key => Object.keys(defaults).indexOf(key) === -1) - .map(key => [key, params[key]]) - ); - -/** - * helper function to merge old and new params together - * @param {object} namespaced params object old params with defaults filtered out + * Merge old and new params together, joining values into arrays where necessary + * @param {object} namespaced params object of old params * @param {object} namespaced params object of new params * @return {object} merged namespaced params object */ -// TODO: BUG? newParam that doesn't exist in oldParams isn't added(?) -const mergeParams = (oldParams, newParams) => { - const merged = arrayToObject( - Object.keys(oldParams).map(key => { - const oldVal = oldParams[key]; - const newVal = newParams[key]; - return [key, mergeParam(oldVal, newVal)]; - }) - ); +export function mergeParams(oldParams, newParams) { + const merged = {}; + Object.keys(oldParams).forEach(key => { + merged[key] = mergeParam(oldParams[key], newParams[key]); + }); Object.keys(newParams).forEach(key => { if (!merged[key]) { merged[key] = newParams[key]; @@ -344,7 +252,6 @@ const mergeParams = (oldParams, newParams) => { }); return merged; } -export { mergeParams as _mergeParams }; function mergeParam(oldVal, newVal) { if (!newVal) { @@ -353,22 +260,33 @@ function mergeParam(oldVal, newVal) { if (!oldVal) { return newVal; } + let merged; if (Array.isArray(oldVal)) { - oldVal.push(newVal); - return oldVal; + merged = oldVal.concat(newVal); + } else { + merged = ([oldVal]).concat(newVal); } - return [oldVal, newVal]; + return dedupeArray(merged); +} + +function dedupeArray(arr) { + const deduped = [...new Set(arr)]; + if (deduped.length === 1) { + return deduped[0]; + } + return deduped; } /** - * helper function to get new params that are not in merged params - * @param {object} namespaced params object of merged params + * Join old and new params together, replacing old values with new ones where + * necessary + * @param {object} namespaced params object of old params * @param {object} namespaced params object of new params - * @return {object} remaining new namespaced params object + * @return {object} joined namespaced params object */ -const getRemainingNewParams = (mergedParams, newParams) => - arrayToObject( - Object.keys(newParams) - .filter(key => Object.keys(mergedParams).indexOf(key) === -1) - .map(key => [key, newParams[key]]) - ); +export function replaceParams(oldParams, newParams) { + return { + ...oldParams, + ...newParams, + }; +} diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index d97ebcffd8..c0acb8fbd1 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -7,7 +7,8 @@ import { removeParams, _stringToObject, _addDefaultsToObject, - _mergeParams, + mergeParams, + replaceParams, } from './qs'; describe('qs (qs.js)', () => { @@ -91,7 +92,7 @@ describe('qs (qs.js)', () => { const params = { page: 1, foo: 'bar', - } + }; expect(encodeNonDefaultQueryString(conf, params)).toEqual('item.foo=bar'); }); @@ -287,179 +288,6 @@ describe('qs (qs.js)', () => { }); }); - describe('addParams', () => { - test('should add query params', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: 'bar', page: 3, page_size: 15 }; - const newParams = { bag: 'boom' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: 'bar', - bag: 'boom', - page: 3, - page_size: 15, - }); - }); - - test('should add query params with duplicates', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { baz: 'boom' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang', 'boom'], - page: 3, - page_size: 15, - }); - }); - - test('should replace query params that have defaults', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { page: 5 }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang'], - page: 5, - page_size: 15, - }); - }); - - test('should replace query params that match defaults', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { page_size: 5 }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang'], - page: 3, - page_size: 5, - }); - }); - - test('should add multiple params', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { baz: 'bust', pat: 'pal' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang', 'bust'], - pat: 'pal', - page: 3, - page_size: 15, - }); - }); - - test('should convert param to array when merging', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: 'bar', page: 3, page_size: 15 }; - const newParams = { baz: 'bust', pat: 'pal' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bust'], - pat: 'pal', - page: 3, - page_size: 15, - }); - }); - - test('should add namespaced query params', () => { - const config = { - namespace: 'item', - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: 'bar', page: 3, page_size: 15 }; - const newParams = { bag: 'boom' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: 'bar', - bag: 'boom', - page: 3, - page_size: 15, - }); - }); - - test('should not include other namespaced query params when adding', () => { - const config = { - namespace: 'item', - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: 'bar', page: 1, page_size: 15 }; - const newParams = { bag: 'boom' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: 'bar', - bag: 'boom', - page: 1, - page_size: 15, - }); - }); - - test('should add namespaced query params with duplicates', () => { - const config = { - namespace: 'item', - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { baz: 'boom' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang', 'boom'], - page: 3, - page_size: 15, - }); - }); - - test('should replace namespaced query params that are defaults', () => { - const config = { - namespace: 'item', - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { page: 5 }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang'], - page: 5, - page_size: 15, - }); - }); - - test('should add multiple namespaced params', () => { - const config = { - namespace: 'item', - defaultParams: { page: 1, page_size: 15 }, - integerFields: ['page', 'page_size'], - }; - const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { baz: 'bust', pat: 'pal' }; - expect(addParams(config, oldParams, newParams)).toEqual({ - baz: ['bar', 'bang', 'bust'], - pat: 'pal', - page: 3, - page_size: 15, - }); - }); - }); - describe('removeParams', () => { test('should remove query params', () => { const config = { @@ -726,7 +554,7 @@ describe('qs (qs.js)', () => { const newParams = { foo: 'two', }; - expect(_mergeParams(oldParams, newParams)).toEqual({ + expect(mergeParams(oldParams, newParams)).toEqual({ foo: ['one', 'two'], }); }); @@ -734,28 +562,137 @@ describe('qs (qs.js)', () => { it('should retain unaltered params', () => { const oldParams = { foo: 'one', - bar: 'baz' + bar: 'baz', }; const newParams = { foo: 'two', }; - expect(_mergeParams(oldParams, newParams)).toEqual({ + expect(mergeParams(oldParams, newParams)).toEqual({ foo: ['one', 'two'], bar: 'baz', }); }); - it('should merge objects', () => { + it('should gather params from both objects', () => { const oldParams = { one: 'one', }; const newParams = { two: 'two', }; - expect(_mergeParams(oldParams, newParams)).toEqual({ + expect(mergeParams(oldParams, newParams)).toEqual({ one: 'one', two: 'two', }); }); + + it('should append to array', () => { + const oldParams = { + foo: ['one', 'two'], + }; + const newParams = { + foo: 'three', + }; + expect(mergeParams(oldParams, newParams)).toEqual({ + foo: ['one', 'two', 'three'], + }); + }); + + it('should append array to existing value', () => { + const oldParams = { + foo: 'one', + }; + const newParams = { + foo: ['two', 'three'], + }; + expect(mergeParams(oldParams, newParams)).toEqual({ + foo: ['one', 'two', 'three'], + }); + }); + + it('should merge two arrays', () => { + const oldParams = { + foo: ['one', 'two'], + }; + const newParams = { + foo: ['three', 'four'], + }; + expect(mergeParams(oldParams, newParams)).toEqual({ + foo: ['one', 'two', 'three', 'four'], + }); + }); + + it('should prevent exact duplicates', () => { + const oldParams = { foo: 'one' }; + const newParams = { foo: 'one' }; + expect(mergeParams(oldParams, newParams)).toEqual({ foo: 'one' }); + }); + + it('should prevent exact duplicates in arrays', () => { + const oldParams = { foo: ['one', 'three'] }; + const newParams = { foo: ['one', 'two'] }; + expect(mergeParams(oldParams, newParams)).toEqual({ + foo: ['one', 'three', 'two'], + }); + }); + + it('should add multiple params', () => { + const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; + const newParams = { baz: 'bust', pat: 'pal' }; + expect(mergeParams(oldParams, newParams)).toEqual({ + baz: ['bar', 'bang', 'bust'], + pat: 'pal', + page: 3, + page_size: 15, + }); + }) + }); + + describe('replaceParams', () => { + it('should collect params into one object', () => { + const oldParams = { foo: 'one' }; + const newParams = { bar: 'two' }; + expect(replaceParams(oldParams, newParams)).toEqual({ + foo: 'one', + bar: 'two', + }); + }); + + it('should retain unaltered params', () => { + const oldParams = { + foo: 'one', + bar: 'baz', + }; + const newParams = { foo: 'two' }; + expect(replaceParams(oldParams, newParams)).toEqual({ + foo: 'two', + bar: 'baz', + }); + }); + + it('should override old values with new ones', () => { + const oldParams = { + foo: 'one', + bar: 'three', + }; + const newParams = { + foo: 'two', + baz: 'four', + }; + expect(replaceParams(oldParams, newParams)).toEqual({ + foo: 'two', + bar: 'three', + baz: 'four', + }); + }); + + it('should handle exact duplicates', () => { + const oldParams = { foo: 'one' }; + const newParams = { foo: 'one', bar: 'two' }; + expect(replaceParams(oldParams, newParams)).toEqual({ + foo: 'one', + bar: 'two', + }); + }); }); }); From 508535be6662300dd6cf8d26e3e00acdd6dafa5e Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Sep 2019 11:42:00 -0700 Subject: [PATCH 5/7] rework removeParams --- awx/ui_next/src/util/qs.js | 191 +++++++++++++++----------------- awx/ui_next/src/util/qs.test.js | 99 +++++++++++++---- 2 files changed, 164 insertions(+), 126 deletions(-) diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index 75df652120..60d68127c2 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -36,6 +36,53 @@ export function parseQueryString(config, queryString) { return addDefaultsToObject(config, params); } +function stringToObject(config, qs) { + const params = {}; + qs.replace(/^\?/, '') + .split('&') + .map(s => s.split('=')) + .forEach(([nsKey, rawValue]) => { + if (!nsKey || !namespaceMatches(config.namespace, nsKey)) { + return; + } + const key = config.namespace + ? decodeURIComponent(nsKey.substr(config.namespace.length + 1)) + : decodeURIComponent(nsKey); + const value = parseValue(config, key, rawValue); + params[key] = mergeParam(params[key], value); + }); + return params; +}; +export { stringToObject as _stringToObject }; + +/** + * helper function to check the namespace of a param is what you expec + * @param {string} namespace to append to params + * @param {object} params object to append namespace to + * @return {object} params object with namespaced keys + */ +const namespaceMatches = (namespace, fieldname) => { + if (!namespace) return !fieldname.includes('.'); + + return fieldname.startsWith(`${namespace}.`); +}; + +function parseValue(config, key, rawValue) { + if (config.integerFields && config.integerFields.some(v => v === key)) { + return parseInt(rawValue, 10); + } + // TODO: parse dateFields into date format? + return decodeURIComponent(rawValue); +} + +function addDefaultsToObject(config, params) { + return { + ...config.defaultParams, + ...params, + }; +} +export { addDefaultsToObject as _addDefaultsToObject }; + /** * Convert query param object to url query string * Used to encode params for interacting with the api @@ -97,100 +144,13 @@ export const encodeNonDefaultQueryString = (config, params) => { .join('&'); }; -/** - * Removes params from the search string and returns the updated list of params - * @param {object} qs config object (used for getting defaults, current query params etc.) - * @param {object} object with params from existing search - * @param {object} object with new params to remove - * @return {object} query param object - */ -export function removeParams(config, oldParams, paramsToRemove) { - const paramsEntries = []; - Object.entries(oldParams).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(val => { - paramsEntries.push([key, val]); - }); - } else { - paramsEntries.push([key, value]); - } - }); - const paramsToRemoveEntries = Object.entries(paramsToRemove); - const remainingEntries = paramsEntries.filter( - ([key, value]) => - paramsToRemoveEntries.filter( - ([newKey, newValue]) => key === newKey && value === newValue - ).length === 0 - ); - const remainingObject = arrayToObject(remainingEntries); - const defaultEntriesLeftover = Object.entries(config.defaultParams).filter( - ([key]) => !remainingObject[key] - ); - const finalParamsEntries = remainingEntries; - defaultEntriesLeftover.forEach(value => { - finalParamsEntries.push(value); - }); - return arrayToObject(finalParamsEntries); -} - -const stringToObject = (config, qs) => { - const params = {}; - qs.replace(/^\?/, '') - .split('&') - .map(s => s.split('=')) - .forEach(([nsKey, rawValue]) => { - if (!nsKey || !namespaceMatches(config.namespace, nsKey)) { - return; - } - const key = decodeURIComponent(nsKey.substr(config.namespace.length + 1)); - const value = parseValue(config, key, rawValue); - params[key] = mergeParam(params[key], value); - }); - return params; -}; -export { stringToObject as _stringToObject }; - -function parseValue(config, key, rawValue) { - if (config.integerFields && config.integerFields.some(v => v === key)) { - return parseInt(rawValue, 10); - } - // TODO: parse dateFields into date format? - return decodeURIComponent(rawValue); -} - -function addDefaultsToObject(config, params) { - return { - ...config.defaultParams, - ...params, - }; -} -export { addDefaultsToObject as _addDefaultsToObject }; - -/** - * helper function used to convert from - * Object.entries format ([ [ key, value ], ... ]) to object - * @param {array} array in the format [ [ key, value ], ...] - * @return {object} object in the forms { key: value, ... } - */ -const arrayToObject = entriesArr => - entriesArr.reduce((acc, [key, value]) => { - if (acc[key] && Array.isArray(acc[key])) { - acc[key].push(value); - } else if (acc[key]) { - acc[key] = [acc[key], value]; - } else { - acc[key] = value; - } - return acc; - }, {}); - /** * helper function to namespace params object * @param {string} namespace to append to params * @param {object} params object to append namespace to * @return {object} params object with namespaced keys */ -const namespaceParams = (namespace, params = {}) => { +const namespaceParams = (namespace, params) => { if (!namespace) return params; const namespaced = {}; @@ -198,19 +158,7 @@ const namespaceParams = (namespace, params = {}) => { namespaced[`${namespace}.${key}`] = params[key]; }); - return namespaced || {}; -}; - -/** - * helper function to check the namespace of a param is what you expec - * @param {string} namespace to append to params - * @param {object} params object to append namespace to - * @return {object} params object with namespaced keys - */ -const namespaceMatches = (namespace, fieldname) => { - if (!namespace) return !fieldname.includes('.'); - - return fieldname.startsWith(`${namespace}.`); + return namespaced; }; /** @@ -234,6 +182,45 @@ const paramValueIsEqual = (one, two) => { return isEqual; }; +/** + * Removes params from the search string and returns the updated list of params + * @param {object} qs config object (used for getting defaults, current query params etc.) + * @param {object} object with params from existing search + * @param {object} object with new params to remove + * @return {object} query param object + */ +export function removeParams(config, oldParams, paramsToRemove) { + const updated = { + ...config.defaultParams, + }; + Object.keys(oldParams).forEach(key => { + const value = removeParam(oldParams[key], paramsToRemove[key]); + if (value) { + updated[key] = value; + } + }); + return updated; +} + +function removeParam(oldVal, deleteVal) { + if (oldVal === deleteVal) { + return null; + } + if (Array.isArray(deleteVal)) { + return deleteVal.reduce(removeParam, oldVal); + } + if (Array.isArray(oldVal)) { + const index = oldVal.indexOf(deleteVal); + if (index > -1) { + oldVal.splice(index, 1); + } + if (oldVal.length === 1) { + return oldVal[0]; + } + } + return oldVal; +} + /** * Merge old and new params together, joining values into arrays where necessary * @param {object} namespaced params object of old params @@ -264,7 +251,7 @@ function mergeParam(oldVal, newVal) { if (Array.isArray(oldVal)) { merged = oldVal.concat(newVal); } else { - merged = ([oldVal]).concat(newVal); + merged = [oldVal].concat(newVal); } return dedupeArray(merged); } diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index c0acb8fbd1..6ae8e48ba0 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -3,7 +3,6 @@ import { encodeNonDefaultQueryString, parseQueryString, getQSConfig, - addParams, removeParams, _stringToObject, _addDefaultsToObject, @@ -286,6 +285,19 @@ describe('qs (qs.js)', () => { baz: ['one', 'two', 'three'], }); }); + + test('should handle non-namespaced params', () => { + const config = { + namespace: null, + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['page', 'page_size'], + }; + const query = '?item.baz=bar&page=3'; + expect(parseQueryString(config, query)).toEqual({ + page: 3, + page_size: 15, + }); + }); }); describe('removeParams', () => { @@ -296,8 +308,8 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: 'bar', page: 3, bag: 'boom', page_size: 15 }; - const newParams = { bag: 'boom' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { bag: 'boom' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: 'bar', page: 3, page_size: 15, @@ -311,8 +323,8 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { baz: 'bar' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bar' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: 'bang', page: 3, page_size: 15, @@ -330,14 +342,33 @@ describe('qs (qs.js)', () => { page: 3, page_size: 15, }; - const newParams = { baz: 'bar' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bar' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: ['bang', 'bust'], page: 3, page_size: 15, }); }); + test('should remove multiple values from query params (array -> smaller array)', () => { + const config = { + namespace: null, + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['page', 'page_size'], + }; + const oldParams = { + baz: ['bar', 'bang', 'bust'], + page: 3, + page_size: 15, + }; + const toRemove = { baz: ['bang', 'bar'] }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ + baz: 'bust', + page: 3, + page_size: 15, + }); + }); + test('should reset query params that have default keys back to default values', () => { const config = { namespace: null, @@ -345,8 +376,8 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { page: 3 }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { page: 3 }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: ['bar', 'bang'], page: 1, page_size: 15, @@ -365,8 +396,8 @@ describe('qs (qs.js)', () => { page: 3, page_size: 15, }; - const newParams = { baz: 'bust', pat: 'pal' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bust', pat: 'pal' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: ['bar', 'bang'], page: 3, page_size: 15, @@ -380,8 +411,8 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: 'bar', page: 3, page_size: 15 }; - const newParams = { baz: 'bar' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bar' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ page: 3, page_size: 15, }); @@ -394,8 +425,8 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: 'bar', page: 1, page_size: 15 }; - const newParams = { baz: 'bar' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bar' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ page: 1, page_size: 15, }); @@ -408,8 +439,8 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { baz: 'bar' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bar' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: 'bang', page: 3, page_size: 15, @@ -427,8 +458,8 @@ describe('qs (qs.js)', () => { page: 3, page_size: 15, }; - const newParams = { baz: 'bar' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bar' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: ['bang', 'bust'], page: 3, page_size: 15, @@ -442,14 +473,34 @@ describe('qs (qs.js)', () => { integerFields: ['page', 'page_size'], }; const oldParams = { baz: ['bar', 'bang'], page: 3, page_size: 15 }; - const newParams = { page: 3 }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { page: 3 }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: ['bar', 'bang'], page: 1, page_size: 15, }); }); + test('should retain long array values', () => { + const config = { + namespace: 'item', + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['page', 'page_size'], + }; + const oldParams = { + baz: ['one', 'two', 'three'], + page: 3, + bag: 'boom', + page_size: 15, + }; + const toRemove = { bag: 'boom' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ + baz: ['one', 'two', 'three'], + page: 3, + page_size: 15, + }); + }); + test('should remove multiple namespaced params', () => { const config = { namespace: 'item', @@ -462,8 +513,8 @@ describe('qs (qs.js)', () => { page: 3, page_size: 15, }; - const newParams = { baz: 'bust', pat: 'pal' }; - expect(removeParams(config, oldParams, newParams)).toEqual({ + const toRemove = { baz: 'bust', pat: 'pal' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: ['bar', 'bang'], page: 3, page_size: 15, @@ -645,7 +696,7 @@ describe('qs (qs.js)', () => { page: 3, page_size: 15, }); - }) + }); }); describe('replaceParams', () => { From 1f149bb086078f75ff2c8737b93a2939797e33e3 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 19 Sep 2019 11:51:51 -0700 Subject: [PATCH 6/7] refactor encodeNoneDefaultQueryString --- awx/ui_next/src/util/qs.js | 47 ++++---------------------------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index 60d68127c2..e4b8b3cf75 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -52,7 +52,7 @@ function stringToObject(config, qs) { params[key] = mergeParam(params[key], value); }); return params; -}; +} export { stringToObject as _stringToObject }; /** @@ -120,28 +120,10 @@ function encodeValue(key, value) { export const encodeNonDefaultQueryString = (config, params) => { if (!params) return ''; - const namespacedParams = namespaceParams(config.namespace, params); - const namespacedDefaults = namespaceParams( - config.namespace, - config.defaultParams + const paramsWithoutDefaults = removeParams({}, params, config.defaultParams); + return encodeQueryString( + namespaceParams(config.namespace, paramsWithoutDefaults) ); - const namespacedDefaultKeys = Object.keys(namespacedDefaults); - const namespacedParamsWithoutDefaultsKeys = Object.keys( - namespacedParams - ).filter( - key => - namespacedDefaultKeys.indexOf(key) === -1 || - !paramValueIsEqual(namespacedParams[key], namespacedDefaults[key]) - ); - - return namespacedParamsWithoutDefaultsKeys - .sort() - .filter(key => namespacedParams[key] !== null) - .map(key => { - return [key, namespacedParams[key]]; - }) - .map(([key, value]) => encodeValue(key, value)) - .join('&'); }; /** @@ -161,27 +143,6 @@ const namespaceParams = (namespace, params) => { return namespaced; }; -/** - * helper function to check the value of a param is equal to another - * @param {string or number or array} param value one - * @param {string or number or array} params value two - * @return {boolean} true if values are equal - */ -const paramValueIsEqual = (one, two) => { - let isEqual = false; - - if (Array.isArray(one) && Array.isArray(two)) { - isEqual = one.filter(val => two.indexOf(val) > -1).length === one.length; - } else if ( - (typeof one === 'string' && typeof two === 'string') || - (typeof one === 'number' && typeof two === 'number') - ) { - isEqual = one === two; - } - - return isEqual; -}; - /** * Removes params from the search string and returns the updated list of params * @param {object} qs config object (used for getting defaults, current query params etc.) From 5d840af2234135afaee7687026681d87d26a039b Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 20 Sep 2019 13:45:22 -0700 Subject: [PATCH 7/7] fix typo, clarify test names --- awx/ui_next/src/util/qs.js | 2 +- awx/ui_next/src/util/qs.test.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index e4b8b3cf75..e7b38bad36 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -56,7 +56,7 @@ function stringToObject(config, qs) { export { stringToObject as _stringToObject }; /** - * helper function to check the namespace of a param is what you expec + * helper function to check the namespace of a param is what you expect * @param {string} namespace to append to params * @param {object} params object to append namespace to * @return {object} params object with namespaced keys diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index 6ae8e48ba0..c50ef8d217 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -115,7 +115,6 @@ describe('qs (qs.js)', () => { test('should get default QS config object', () => { expect(getQSConfig('organization')).toEqual({ namespace: 'organization', - dateFields: ['modified', 'created'], defaultParams: { page: 1, page_size: 5, order_by: 'name' }, integerFields: ['page', 'page_size'], dateFields: ['modified', 'created'], @@ -133,7 +132,6 @@ describe('qs (qs.js)', () => { }; expect(getQSConfig('inventory', defaults)).toEqual({ namespace: 'inventory', - dateFields: ['modified', 'created'], defaultParams: { page: 1, page_size: 15 }, integerFields: ['page', 'page_size'], dateFields: ['modified', 'created'], @@ -637,7 +635,7 @@ describe('qs (qs.js)', () => { }); }); - it('should append to array', () => { + it('should append value to existing array', () => { const oldParams = { foo: ['one', 'two'], };