Commit de53e7d5 authored by Michael Lunøe's avatar Michael Lunøe

Merge branch 'mrincon-support-plus-as-spaces' into 'master'

Update queryToObject to replace plus with spaces

See merge request gitlab-org/gitlab!63753
parents 68746a8e c13a6d22
......@@ -31,7 +31,7 @@ function organizeQuery(obj, isFallbackKey = false) {
}
function format(searchTerm, isFallbackKey = false) {
const queryObject = queryToObject(searchTerm);
const queryObject = queryToObject(searchTerm, { legacySpacesDecode: true });
const organizeQueryObject = organizeQuery(queryObject, isFallbackKey);
const formattedQuery = objectToQuery(organizeQueryObject);
......
......@@ -414,29 +414,35 @@ export function getWebSocketUrl(path) {
*
* @param {String} query from "document.location.search"
* @param {Object} options
* @param {Boolean} options.gatherArrays - gather array values into an Array
* @param {Boolean?} options.gatherArrays - gather array values into an Array
* @param {Boolean?} options.legacySpacesDecode - (deprecated) plus symbols (+) are not replaced with spaces, false by default
* @returns {Object}
*
* ex: "?one=1&two=2" into {one: 1, two: 2}
*/
export function queryToObject(query, options = {}) {
const { gatherArrays = false } = options;
export function queryToObject(query, { gatherArrays = false, legacySpacesDecode = false } = {}) {
const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
const [key, value] = curr.split('=');
if (value === undefined) {
return accumulator;
}
const decodedValue = decodeURIComponent(value);
const decodedValue = legacySpacesDecode ? decodeURIComponent(value) : decodeUrlParameter(value);
if (gatherArrays && key.endsWith('[]')) {
const decodedKey = decodeURIComponent(key.slice(0, -2));
const decodedKey = legacySpacesDecode
? decodeURIComponent(key.slice(0, -2))
: decodeUrlParameter(key.slice(0, -2));
if (!Array.isArray(accumulator[decodedKey])) {
accumulator[decodedKey] = [];
}
accumulator[decodedKey].push(decodedValue);
} else {
accumulator[decodeURIComponent(key)] = decodedValue;
const decodedKey = legacySpacesDecode ? decodeURIComponent(key) : decodeUrlParameter(key);
accumulator[decodedKey] = decodedValue;
}
return accumulator;
......
......@@ -175,7 +175,7 @@ export const graphDataValidatorForAnomalyValues = (graphData) => {
* Returns `null` if no parameters form a time range.
*/
export const timeRangeFromUrl = (search = window.location.search) => {
const params = queryToObject(search);
const params = queryToObject(search, { legacySpacesDecode: true });
return timeRangeFromParams(params);
};
......@@ -228,7 +228,7 @@ export const convertVariablesForURL = (variables) =>
* @returns {Object} The custom variables defined by the user in the URL
*/
export const templatingVariablesFromUrl = (search = window.location.search) => {
const params = queryToObject(search);
const params = queryToObject(search, { legacySpacesDecode: true });
// pick the params with variable prefix
const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX));
// remove the prefix before storing in the Vuex store
......@@ -289,7 +289,7 @@ export const timeRangeToUrl = (timeRange, url = window.location.href) => {
* @throws Will throw an error if Panel cannot be located.
*/
export const expandedPanelPayloadFromUrl = (dashboard, search = window.location.search) => {
const params = queryToObject(search);
const params = queryToObject(search, { legacySpacesDecode: true });
// Search for the panel if any of the search params is identified
if (params.group || params.title || params.y_label) {
......
import { queryToObject } from '~/lib/utils/url_utility';
import { FILTERED_SEARCH_TERM } from './constants';
export const getQueryParams = (query) => queryToObject(query, { gatherArrays: true });
export const getQueryParams = (query) =>
queryToObject(query, { gatherArrays: true, legacySpacesDecode: true });
export const keyValueToFilterToken = (type, data) => ({ type, value: { data } });
......
......@@ -8,10 +8,7 @@ import createStore from './store';
import { initTopbar } from './topbar';
export const initSearchApp = () => {
// Similar to url_utility.decodeUrlParameter
// Our query treats + as %20. This replaces the query + symbols with %20.
const sanitizedSearch = window.location.search.replace(/\+/g, '%20');
const query = queryToObject(sanitizedSearch);
const query = queryToObject(window.location.search);
const store = createStore({ query });
......
......@@ -142,11 +142,11 @@ function extractNameAndOperator(filterName) {
* '?myFilterName=foo'
* gets translated into:
* { myFilterName: { value: 'foo', operator: '=' } }
* @param {String} query URL quert string, e.g. from `window.location.search`
* @param {String} query URL query string, e.g. from `window.location.search`
* @return {Object} filter object with filter names and their values
*/
export function urlQueryToFilter(query = '') {
const filters = queryToObject(query, { gatherArrays: true });
const filters = queryToObject(query, { gatherArrays: true, legacySpacesDecode: true });
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
......
......@@ -5,7 +5,7 @@ import * as types from './mutation_types';
export const initializeAuditEvents = ({ commit }) => {
commit(
types.INITIALIZE_AUDIT_EVENTS,
parseAuditEventSearchQuery(queryToObject(window.location.search)),
parseAuditEventSearchQuery(queryToObject(window.location.search, { legacySpacesDecode: true })),
);
};
......
......@@ -227,7 +227,7 @@ export default {
},
},
created() {
const params = queryToObject(window.location.search);
const params = queryToObject(window.location.search, { legacySpacesDecode: true });
this.selectedSiteProfileId = params.site_profile_id
? convertToGraphQLId(TYPE_SITE_PROFILE, params.site_profile_id)
......
......@@ -650,45 +650,24 @@ describe('URL utility', () => {
});
describe('queryToObject', () => {
it('converts search query into an object', () => {
const searchQuery = '?one=1&two=2';
expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
});
it('removes undefined values from the search query', () => {
const searchQuery = '?one=1&two=2&three';
expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
});
describe('with gatherArrays=false', () => {
it('overwrites values with the same array-key and does not change the key', () => {
const searchQuery = '?one[]=1&one[]=2&two=2&two=3';
expect(urlUtils.queryToObject(searchQuery)).toEqual({ 'one[]': '2', two: '3' });
});
});
describe('with gatherArrays=true', () => {
const options = { gatherArrays: true };
it('gathers only values with the same array-key and strips `[]` from the key', () => {
const searchQuery = '?one[]=1&one[]=2&two=2&two=3';
expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: ['1', '2'], two: '3' });
});
it('overwrites values with the same array-key name', () => {
const searchQuery = '?one=1&one[]=2&two=2&two=3';
expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: ['2'], two: '3' });
});
it('overwrites values with the same key name', () => {
const searchQuery = '?one[]=1&one=2&two=2&two=3';
expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: '2', two: '3' });
});
it.each`
case | query | options | result
${'converts query'} | ${'?one=1&two=2'} | ${undefined} | ${{ one: '1', two: '2' }}
${'converts query without ?'} | ${'one=1&two=2'} | ${undefined} | ${{ one: '1', two: '2' }}
${'removes undefined values'} | ${'?one=1&two=2&three'} | ${undefined} | ${{ one: '1', two: '2' }}
${'overwrites values with same key and does not change key'} | ${'?one[]=1&one[]=2&two=2&two=3'} | ${undefined} | ${{ 'one[]': '2', two: '3' }}
${'gathers values with the same array-key, strips `[]` from key'} | ${'?one[]=1&one[]=2&two=2&two=3'} | ${{ gatherArrays: true }} | ${{ one: ['1', '2'], two: '3' }}
${'overwrites values with the same array-key name'} | ${'?one=1&one[]=2&two=2&two=3'} | ${{ gatherArrays: true }} | ${{ one: ['2'], two: '3' }}
${'overwrites values with the same key name'} | ${'?one[]=1&one=2&two=2&two=3'} | ${{ gatherArrays: true }} | ${{ one: '2', two: '3' }}
${'ignores plus symbols'} | ${'?search=a+b'} | ${{ legacySpacesDecode: true }} | ${{ search: 'a+b' }}
${'ignores plus symbols in keys'} | ${'?search+term=a'} | ${{ legacySpacesDecode: true }} | ${{ 'search+term': 'a' }}
${'ignores plus symbols when gathering arrays'} | ${'?search[]=a+b'} | ${{ gatherArrays: true, legacySpacesDecode: true }} | ${{ search: ['a+b'] }}
${'replaces plus symbols with spaces'} | ${'?search=a+b'} | ${undefined} | ${{ search: 'a b' }}
${'replaces plus symbols in keys with spaces'} | ${'?search+term=a'} | ${undefined} | ${{ 'search term': 'a' }}
${'replaces plus symbols when gathering arrays'} | ${'?search[]=a+b'} | ${{ gatherArrays: true }} | ${{ search: ['a b'] }}
${'replaces plus symbols when gathering arrays for values with same key'} | ${'?search[]=a+b&search[]=c+d'} | ${{ gatherArrays: true }} | ${{ search: ['a b', 'c d'] }}
`('$case', ({ query, options, result }) => {
expect(urlUtils.queryToObject(query, options)).toEqual(result);
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment