diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.jsx index d5e359c3bc..b974f1d410 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.jsx @@ -6,11 +6,10 @@ import { Toolbar, ToolbarContent } from '@patternfly/react-core'; import DataListToolbar from '../DataListToolbar'; import { - encodeNonDefaultQueryString, parseQueryString, mergeParams, - replaceParams, removeParams, + updateQueryString, } from '../../util/qs'; import { QSConfig, SearchColumns, SortColumns } from '../../types'; @@ -38,60 +37,57 @@ class ListHeader extends React.Component { handleSearch(key, value) { const { location, qsConfig } = this.props; - let params = parseQueryString(qsConfig, location.search); - params = mergeParams(params, { [key]: value }); - params = replaceParams(params, { page: 1 }); - this.pushHistoryState(params); + const params = parseQueryString(qsConfig, location.search); + const qs = updateQueryString(qsConfig, location.search, { + ...mergeParams(params, { [key]: value }), + page: 1, + }); + this.pushHistoryState(qs); } handleReplaceSearch(key, value) { const { location, qsConfig } = this.props; - const oldParams = parseQueryString(qsConfig, location.search); - this.pushHistoryState(replaceParams(oldParams, { [key]: value })); + const qs = updateQueryString(qsConfig, location.search, { + [key]: value, + }); + this.pushHistoryState(qs); } handleRemove(key, value) { const { location, qsConfig } = this.props; - let oldParams = parseQueryString(qsConfig, location.search); - if (parseInt(value, 10)) { - oldParams = removeParams(qsConfig, oldParams, { - [key]: parseInt(value, 10), - }); - } - this.pushHistoryState(removeParams(qsConfig, oldParams, { [key]: value })); + const oldParams = parseQueryString(qsConfig, location.search); + const updatedParams = removeParams(qsConfig, oldParams, { + [key]: value, + }); + const qs = updateQueryString(qsConfig, location.search, updatedParams); + this.pushHistoryState(qs); } handleRemoveAll() { - // remove everything in oldParams except for page_size and order_by const { location, qsConfig } = this.props; const oldParams = parseQueryString(qsConfig, location.search); - const oldParamsClone = { ...oldParams }; - delete oldParamsClone.page_size; - delete oldParamsClone.order_by; - this.pushHistoryState(removeParams(qsConfig, oldParams, oldParamsClone)); + Object.keys(oldParams).forEach(key => { + oldParams[key] = null; + }); + delete oldParams.page_size; + delete oldParams.order_by; + const qs = updateQueryString(qsConfig, location.search, oldParams); + this.pushHistoryState(qs); } handleSort(key, order) { const { location, qsConfig } = this.props; - const oldParams = parseQueryString(qsConfig, location.search); - this.pushHistoryState( - replaceParams(oldParams, { - order_by: order === 'ascending' ? key : `-${key}`, - page: null, - }) - ); + const qs = updateQueryString(qsConfig, location.search, { + order_by: order === 'ascending' ? key : `-${key}`, + page: null, + }); + this.pushHistoryState(qs); } - pushHistoryState(params) { - const { history, qsConfig } = this.props; + pushHistoryState(queryString) { + const { history } = this.props; const { pathname } = history.location; - const nonNamespacedParams = parseQueryString({}, history.location.search); - const encodedParams = encodeNonDefaultQueryString( - qsConfig, - params, - nonNamespacedParams - ); - history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); + history.push(queryString ? `${pathname}?${queryString}` : pathname); } render() { diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx index d501418c44..f7199fe4d4 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx @@ -51,7 +51,7 @@ describe('ListHeader', () => { expect(history.location.search).toEqual(''); }); - test('should test clear all', () => { + test('should clear all', () => { const query = '?item.page_size=5&item.name=foo'; const history = createMemoryHistory({ initialEntries: [`/organizations/1/teams${query}`], diff --git a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx index aaeed5455b..74a0596de1 100644 --- a/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/PaginatedDataList.jsx @@ -13,11 +13,7 @@ import ContentLoading from '../ContentLoading'; import Pagination from '../Pagination'; import DataListToolbar from '../DataListToolbar'; -import { - encodeNonDefaultQueryString, - parseQueryString, - replaceParams, -} from '../../util/qs'; +import { parseQueryString, updateQueryString } from '../../util/qs'; import { QSConfig, SearchColumns, SortColumns } from '../../types'; @@ -40,7 +36,6 @@ function PaginatedDataList({ pluralizedItemName, showPageSizeOptions, location, - renderToolbar, }) { const { search, pathname } = useLocation(); @@ -51,23 +46,22 @@ function PaginatedDataList({ }; const handleSetPage = (event, pageNumber) => { - const oldParams = parseQueryString(qsConfig, search); - pushHistoryState(replaceParams(oldParams, { page: pageNumber })); + const qs = updateQueryString(qsConfig, search, { + page: pageNumber, + }); + pushHistoryState(qs); }; const handleSetPageSize = (event, pageSize, page) => { - const oldParams = parseQueryString(qsConfig, search); - pushHistoryState(replaceParams(oldParams, { page_size: pageSize, page })); + const qs = updateQueryString(qsConfig, search, { + page_size: pageSize, + page, + }); + pushHistoryState(qs); }; - const pushHistoryState = params => { - const nonNamespacedParams = parseQueryString({}, history.location.search); - const encodedParams = encodeNonDefaultQueryString( - qsConfig, - params, - nonNamespacedParams - ); - history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); + const pushHistoryState = qs => { + history.push(qs ? `${pathname}?${qs}` : pathname); }; const searchColumns = toolbarSearchColumns.length diff --git a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx index a7b076da57..230a61705b 100644 --- a/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx +++ b/awx/ui_next/src/components/PaginatedTable/HeaderRow.jsx @@ -3,11 +3,7 @@ import React from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { Thead, Tr, Th as PFTh } from '@patternfly/react-table'; import styled from 'styled-components'; -import { - encodeNonDefaultQueryString, - parseQueryString, - replaceParams, -} from '../../util/qs'; +import { parseQueryString, updateQueryString } from '../../util/qs'; const Th = styled(PFTh)` --pf-c-table--cell--Overflow: initial; @@ -25,21 +21,11 @@ export default function HeaderRow({ const params = parseQueryString(qsConfig, location.search); const onSort = (key, order) => { - const newParams = replaceParams(params, { + const qs = updateQueryString(qsConfig, location.search, { order_by: order === 'asc' ? key : `-${key}`, page: null, }); - const nonNamespacedParams = parseQueryString({}, history.location.search); - const encodedParams = encodeNonDefaultQueryString( - qsConfig, - newParams, - nonNamespacedParams - ); - history.push( - encodedParams - ? `${location.pathname}?${encodedParams}` - : location.pathname - ); + history.push(qs ? `${location.pathname}?${qs}` : location.pathname); }; const sortKey = params.order_by?.replace('-', ''); diff --git a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx index 871e2bfbb2..7fd76a207d 100644 --- a/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx +++ b/awx/ui_next/src/components/PaginatedTable/PaginatedTable.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { TableComposable, Tbody } from '@patternfly/react-table'; import { t } from '@lingui/macro'; -import { useHistory } from 'react-router-dom'; +import { useLocation, useHistory } from 'react-router-dom'; import ListHeader from '../ListHeader'; import ContentEmpty from '../ContentEmpty'; @@ -14,11 +14,7 @@ import Pagination from '../Pagination'; import DataListToolbar from '../DataListToolbar'; import LoadingSpinner from '../LoadingSpinner'; -import { - encodeNonDefaultQueryString, - parseQueryString, - replaceParams, -} from '../../util/qs'; +import { parseQueryString, updateQueryString } from '../../util/qs'; import { QSConfig, SearchColumns } from '../../types'; function PaginatedTable({ @@ -39,27 +35,26 @@ function PaginatedTable({ emptyContentMessage, ouiaId, }) { + const { search, pathname } = useLocation(); const history = useHistory(); - const pushHistoryState = params => { - const { pathname, search } = history.location; - const nonNamespacedParams = parseQueryString({}, search); - const encodedParams = encodeNonDefaultQueryString( - qsConfig, - params, - nonNamespacedParams - ); - history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); + const pushHistoryState = qs => { + history.push(qs ? `${pathname}?${qs}` : pathname); }; const handleSetPage = (event, pageNumber) => { - const oldParams = parseQueryString(qsConfig, history.location.search); - pushHistoryState(replaceParams(oldParams, { page: pageNumber })); + const qs = updateQueryString(qsConfig, search, { + page: pageNumber, + }); + pushHistoryState(qs); }; const handleSetPageSize = (event, pageSize, page) => { - const oldParams = parseQueryString(qsConfig, history.location.search); - pushHistoryState(replaceParams(oldParams, { page_size: pageSize, page })); + const qs = updateQueryString(qsConfig, search, { + page_size: pageSize, + page, + }); + pushHistoryState(qs); }; const searchColumns = toolbarSearchColumns.length diff --git a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx index d20f02ce20..0fd182f2ae 100644 --- a/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx +++ b/awx/ui_next/src/screens/ActivityStream/ActivityStream.jsx @@ -22,8 +22,7 @@ import useRequest from '../../util/useRequest'; import { getQSConfig, parseQueryString, - replaceParams, - encodeNonDefaultQueryString, + updateQueryString, } from '../../util/qs'; import { ActivityStreamAPI } from '../../api'; @@ -96,16 +95,14 @@ function ActivityStream() { }, [fetchActivityStream]); const pushHistoryState = urlParamsToAdd => { - let searchParams = parseQueryString(QS_CONFIG, location.search); - searchParams = replaceParams(searchParams, { page: 1 }); - const encodedParams = encodeNonDefaultQueryString(QS_CONFIG, searchParams, { + const pageOneQs = updateQueryString(QS_CONFIG, location.search, { + page: 1, + }); + const qs = updateQueryString(null, pageOneQs, { type: urlParamsToAdd.get('type'), }); - history.push( - encodedParams - ? `${location.pathname}?${encodedParams}` - : location.pathname - ); + + history.push(qs ? `${location.pathname}?${qs}` : location.pathname); }; return ( diff --git a/awx/ui_next/src/screens/Host/Host.jsx b/awx/ui_next/src/screens/Host/Host.jsx index dcd1d8c140..9792fd609b 100644 --- a/awx/ui_next/src/screens/Host/Host.jsx +++ b/awx/ui_next/src/screens/Host/Host.jsx @@ -35,7 +35,7 @@ function Host({ setBreadcrumb }) { useEffect(() => { fetchHost(); - }, [fetchHost, location]); + }, [fetchHost, location.pathname]); const tabsArray = [ { diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js index 7797256064..b595cdf028 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js @@ -1,10 +1,6 @@ import { useState, useEffect } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; -import { - parseQueryString, - replaceParams, - encodeNonDefaultQueryString, -} from '../../../util/qs'; +import { parseQueryString, updateQueryString } from '../../../util/qs'; import useWebsocket from '../../../util/useWebsocket'; import useThrottle from '../../../util/useThrottle'; @@ -90,13 +86,10 @@ export default function useWsInventories( ) { // We've deleted the last inventory on this page so we'll // try to navigate back to the previous page - const newParams = encodeNonDefaultQueryString( - qsConfig, - replaceParams(params, { - page: params.page - 1, - }) - ); - history.push(`${location.pathname}?${newParams}`); + const qs = updateQueryString(qsConfig, location.search, { + page: params.page - 1, + }); + history.push(`${location.pathname}?${qs}`); return; } diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx index 19f1b0f751..dfcdc90178 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx @@ -39,12 +39,11 @@ import getRowRangePageSize from './shared/jobOutputUtils'; import { getJobModel, isJobRunning } from '../../../util/jobs'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import { - encodeNonDefaultQueryString, parseQueryString, mergeParams, - replaceParams, removeParams, getQSConfig, + updateQueryString, } from '../../../util/qs'; import useIsMounted from '../../../util/useIsMounted'; @@ -589,35 +588,43 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { }; const handleSearch = (key, value) => { - let params = parseQueryString(QS_CONFIG, location.search); - params = mergeParams(params, { [key]: value }); - pushHistoryState(params); + const params = parseQueryString(QS_CONFIG, location.search); + const qs = updateQueryString( + QS_CONFIG, + location.search, + mergeParams(params, { [key]: value }) + ); + pushHistoryState(qs); }; const handleReplaceSearch = (key, value) => { - const oldParams = parseQueryString(QS_CONFIG, location.search); - pushHistoryState(replaceParams(oldParams, { [key]: value })); + const qs = updateQueryString(QS_CONFIG, location.search, { + [key]: value, + }); + pushHistoryState(qs); }; const handleRemoveSearchTerm = (key, value) => { - let oldParams = parseQueryString(QS_CONFIG, location.search); - if (parseInt(value, 10)) { - oldParams = removeParams(QS_CONFIG, oldParams, { - [key]: parseInt(value, 10), - }); - } - pushHistoryState(removeParams(QS_CONFIG, oldParams, { [key]: value })); + const oldParams = parseQueryString(QS_CONFIG, location.search); + const updatedParams = removeParams(QS_CONFIG, oldParams, { + [key]: value, + }); + const qs = updateQueryString(QS_CONFIG, location.search, updatedParams); + pushHistoryState(qs); }; const handleRemoveAllSearchTerms = () => { const oldParams = parseQueryString(QS_CONFIG, location.search); - pushHistoryState(removeParams(QS_CONFIG, oldParams, { ...oldParams })); + Object.keys(oldParams).forEach(key => { + oldParams[key] = null; + }); + const qs = updateQueryString(QS_CONFIG, location.search, oldParams); + pushHistoryState(qs); }; - const pushHistoryState = params => { + const pushHistoryState = qs => { const { pathname } = history.location; - const encodedParams = encodeNonDefaultQueryString(QS_CONFIG, params); - history.push(encodedParams ? `${pathname}?${encodedParams}` : pathname); + history.push(qs ? `${pathname}?${qs}` : pathname); }; const renderSearchComponent = () => ( diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index 729a28790d..1ef26ea8ce 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -113,44 +113,6 @@ function encodeValue(key, value) { 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 - * @param {object} qs config object for namespacing params, filtering defaults - * @param {object} query param object - * @param {object} any non-namespaced params to append - * @return {string} url query string - */ -export const encodeNonDefaultQueryString = ( - config, - params, - nonNamespacedParams = {} -) => { - if (!params) return ''; - const paramsWithoutDefaults = removeParams({}, params, config.defaultParams); - return encodeQueryString({ - ...namespaceParams(config.namespace, paramsWithoutDefaults), - ...nonNamespacedParams, - }); -}; - -/** - * 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) => { - if (!namespace) return params; - - const namespaced = {}; - Object.keys(params).forEach(key => { - namespaced[`${namespace}.${key}`] = params[key]; - }); - - return namespaced; -}; - /** * 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.) @@ -163,10 +125,19 @@ export function removeParams(config, oldParams, paramsToRemove) { ...config.defaultParams, }; Object.keys(oldParams).forEach(key => { - const value = removeParam(oldParams[key], paramsToRemove[key]); - if (value !== null) { - updated[key] = value; + const valToRemove = paramsToRemove[key]; + const isInt = config.integerFields?.includes(key); + const updatedValue = removeParam( + oldParams[key], + isInt ? parseInt(valToRemove, 10) : valToRemove + ); + if ( + updatedValue == null && + Object.prototype.hasOwnProperty.call(updated, key) + ) { + return; } + updated[key] = updatedValue; }); return updated; } @@ -234,15 +205,42 @@ function dedupeArray(arr) { } /** - * 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} joined namespaced params object + * Update namespaced param(s), returning a new query string. Leaves params + * from other namespaces unaltered + * @param {object} qs config object for namespacing params, filtering defaults + * @param {string} the url query string to update + * @param {object} namespaced params to add or update. use null to indicate + * a param that should be deleted from the query string + * @return {string} url query string */ -export function replaceParams(oldParams, newParams) { - return { - ...oldParams, - ...newParams, - }; +export function updateQueryString(config, queryString, newParams) { + const allParams = parseFullQueryString(queryString); + const { namespace = null, defaultParams = {} } = config || {}; + Object.keys(newParams).forEach(key => { + const val = newParams[key]; + const fullKey = namespace ? `${namespace}.${key}` : key; + if (val === null || val === defaultParams[key]) { + delete allParams[fullKey]; + } else { + allParams[fullKey] = newParams[key]; + } + }); + return encodeQueryString(allParams); +} + +function parseFullQueryString(queryString) { + const allParams = {}; + queryString + .replace(/^\?/, '') + .split('&') + .map(s => s.split('=')) + .forEach(([rawKey, rawValue]) => { + if (!rawKey) { + return; + } + const key = decodeURIComponent(rawKey); + const value = decodeURIComponent(rawValue); + allParams[key] = mergeParam(allParams[key], value); + }); + return allParams; } diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index 763b07434c..a34543e808 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -1,13 +1,12 @@ import { encodeQueryString, - encodeNonDefaultQueryString, parseQueryString, getQSConfig, removeParams, _stringToObject, _addDefaultsToObject, mergeParams, - replaceParams, + updateQueryString, } from './qs'; describe('qs (qs.js)', () => { @@ -47,70 +46,6 @@ describe('qs (qs.js)', () => { }); }); - describe('encodeNonDefaultQueryString', () => { - const config = { - namespace: null, - defaultParams: { page: 1, page_size: 5, order_by: 'name' }, - integerFields: ['page'], - }; - - test('should return the expected queryString', () => { - [ - [null, ''], - [{}, ''], - [{ order_by: 'name', page: 1, page_size: 5 }, ''], - [{ order_by: '-name', page: 1, page_size: 5 }, 'order_by=-name'], - [ - { order_by: '-name', page: 3, page_size: 10 }, - 'order_by=-name&page=3&page_size=10', - ], - [ - { order_by: '-name', page: 3, page_size: 10, foo: 'bar' }, - 'foo=bar&order_by=-name&page=3&page_size=10', - ], - ].forEach(([params, expectedQueryString]) => { - const actualQueryString = encodeNonDefaultQueryString(config, params); - - expect(actualQueryString).toEqual(expectedQueryString); - }); - }); - - test('should omit null values', () => { - const vals = { - order_by: 'foo', - page: null, - }; - expect(encodeNonDefaultQueryString(config, vals)).toEqual('order_by=foo'); - }); - - 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( - 'bar=alpha&bar=beta' - ); - }); - }); - describe('getQSConfig', () => { test('should get default QS config object', () => { expect(getQSConfig('organization')).toEqual({ @@ -340,6 +275,7 @@ describe('qs (qs.js)', () => { baz: 'bar', page: 3, page_size: 15, + bag: null, }); }); @@ -428,6 +364,7 @@ describe('qs (qs.js)', () => { baz: ['bar', 'bang'], page: 3, page_size: 15, + pat: null, }); }); @@ -442,6 +379,7 @@ describe('qs (qs.js)', () => { expect(removeParams(config, oldParams, toRemove)).toEqual({ page: 3, page_size: 15, + baz: null, }); }); @@ -456,6 +394,7 @@ describe('qs (qs.js)', () => { expect(removeParams(config, oldParams, toRemove)).toEqual({ page: 1, page_size: 15, + baz: null, }); }); @@ -525,6 +464,7 @@ describe('qs (qs.js)', () => { baz: ['one', 'two', 'three'], page: 3, page_size: 15, + bag: null, }); }); @@ -545,6 +485,7 @@ describe('qs (qs.js)', () => { baz: ['bar', 'bang'], page: 3, page_size: 15, + pat: null, }); }); @@ -558,10 +499,43 @@ describe('qs (qs.js)', () => { const toRemove = { bag: 'boom' }; expect(removeParams(config, oldParams, toRemove)).toEqual({ baz: '', + bag: null, page: 3, page_size: 15, }); }); + + test('should remove integer fields when given string value', () => { + const config = { + namespace: null, + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['id', 'page', 'page_size'], + }; + const oldParams = { id: 199, foo: 'bar', page: 1, page_size: 15 }; + const toRemove = { id: '199' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ + foo: 'bar', + id: null, + page: 1, + page_size: 15, + }); + }); + + test('should remove integer fields from array when given string value', () => { + const config = { + namespace: null, + defaultParams: { page: 1, page_size: 15 }, + integerFields: ['id', 'page', 'page_size'], + }; + const oldParams = { id: [199, 200], foo: 'bar', page: 1, page_size: 15 }; + const toRemove = { id: '199' }; + expect(removeParams(config, oldParams, toRemove)).toEqual({ + foo: 'bar', + id: 200, + page: 1, + page_size: 15, + }); + }); }); describe('_stringToObject', () => { @@ -763,51 +737,94 @@ describe('qs (qs.js)', () => { }); }); - 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', - }); - }); + describe('updateQueryString', () => { + const config = { + namespace: 'template', + defaultParams: { page: 1, page_size: 5, order_by: 'name' }, + integerFields: ['page'], + }; - 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', - }; + test('should add param to empty query string', () => { const newParams = { - foo: 'two', - baz: 'four', + page: 3, }; - expect(replaceParams(oldParams, newParams)).toEqual({ - foo: 'two', - bar: 'three', - baz: 'four', - }); + expect(updateQueryString(config, '', newParams)).toEqual( + 'template.page=3' + ); }); - it('should handle exact duplicates', () => { - const oldParams = { foo: 'one' }; - const newParams = { foo: 'one', bar: 'two' }; - expect(replaceParams(oldParams, newParams)).toEqual({ - foo: 'one', - bar: 'two', - }); + test('should update namespaced param', () => { + const query = 'template.name__icontains=workflow&template.page=2'; + const newParams = { + page: 3, + }; + expect(updateQueryString(config, query, newParams)).toEqual( + 'template.name__icontains=workflow&template.page=3' + ); + }); + + test('should add new namespaced param', () => { + const query = 'template.name__icontains=workflow&template.page=2'; + const newParams = { + or__type: 'job_template', + }; + expect(updateQueryString(config, query, newParams)).toEqual( + 'template.name__icontains=workflow&template.or__type=job_template&template.page=2' + ); + }); + + test('should maintain non-namespaced param', () => { + const query = 'foo=bar&template.page=2&template.name__icontains=workflow'; + const newParams = { + page: 3, + }; + expect(updateQueryString(config, query, newParams)).toEqual( + 'foo=bar&template.name__icontains=workflow&template.page=3' + ); + }); + + test('should omit null values', () => { + const query = 'template.name__icontains=workflow&template.page=2'; + const newParams = { + page: 3, + name__icontains: null, + }; + expect(updateQueryString(config, query, newParams)).toEqual( + 'template.page=3' + ); + }); + + test('should omit default values', () => { + const query = 'template.page=2'; + const newParams = { + page: 3, + page_size: 5, + }; + expect(updateQueryString(config, query, newParams)).toEqual( + 'template.page=3' + ); + }); + + test('should update non-namespaced param', () => { + const query = + 'activity_stream.name__icontains=workflow&activity_stream.page=2'; + const newParams = { + type: 'job', + }; + expect(updateQueryString(null, query, newParams)).toEqual( + 'activity_stream.name__icontains=workflow&activity_stream.page=2&type=job' + ); + }); + + test('should not alter params of other namespaces', () => { + const query = + 'template.name__icontains=workflow&template.page=2&credential.page=3'; + const newParams = { + page: 3, + }; + expect(updateQueryString(config, query, newParams)).toEqual( + 'credential.page=3&template.name__icontains=workflow&template.page=3' + ); }); }); }); diff --git a/awx/ui_next/src/util/useRequest.js b/awx/ui_next/src/util/useRequest.js index c3328ce7c7..54ffb9d6dd 100644 --- a/awx/ui_next/src/util/useRequest.js +++ b/awx/ui_next/src/util/useRequest.js @@ -1,10 +1,6 @@ import { useEffect, useState, useCallback } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; -import { - parseQueryString, - replaceParams, - encodeNonDefaultQueryString, -} from './qs'; +import { parseQueryString, updateQueryString } from './qs'; import useIsMounted from './useIsMounted'; /* @@ -111,13 +107,10 @@ export function useDeleteItems( } const params = parseQueryString(qsConfig, location.search); if (params.page > 1 && allItemsSelected) { - const newParams = encodeNonDefaultQueryString( - qsConfig, - replaceParams(params, { - page: params.page - 1, - }) - ); - history.push(`${location.pathname}?${newParams}`); + const qs = updateQueryString(qsConfig, location.search, { + page: params.page - 1, + }); + history.push(`${location.pathname}?${qs}`); } else { fetchItems(); }