chore: Replace query-string with URLSearchParams (#613)

Co-authored-by: diana-villalvazo-wgu <dianaximena.villalva@wgu.edu>
This commit is contained in:
diana-villalvazo-wgu
2025-05-13 08:12:06 -07:00
committed by GitHub
parent 11a7512fea
commit a9194261c8
8 changed files with 54 additions and 53 deletions

19
package-lock.json generated
View File

@@ -32,7 +32,6 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
@@ -16469,24 +16468,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/querystringify": { "node_modules/querystringify": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",

View File

@@ -52,7 +52,6 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",

View File

@@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import queryString from 'query-string';
import { ListPageSize, SortKeys } from 'data/constants/app'; import { ListPageSize, SortKeys } from 'data/constants/app';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { StrictDict } from 'utils'; import { StrictDict } from 'utils';
@@ -27,12 +25,13 @@ export const useCourseListData = () => {
const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled); const [sortBy, setSortBy] = module.state.sortBy(SortKeys.enrolled);
const querySearch = queryString.parse(window.location.search, { parseNumbers: true }); const querySearch = new URLSearchParams(window.location.search);
const disablePagination = querySearch.get('disable_pagination');
const { numPages, visibleList } = reduxHooks.useCurrentCourseList({ const { numPages, visibleList } = reduxHooks.useCurrentCourseList({
sortBy, sortBy,
filters, filters,
pageSize: querySearch?.disable_pagination === 1 ? 0 : ListPageSize, pageSize: Number(disablePagination) === 1 ? 0 : ListPageSize,
}); });
const handleRemoveFilter = (filter) => () => removeFilter(filter); const handleRemoveFilter = (filter) => () => removeFilter(filter);

View File

@@ -1,5 +1,3 @@
import queryString from 'query-string';
import { MockUseState } from 'testUtils'; import { MockUseState } from 'testUtils';
import { reduxHooks } from 'hooks'; import { reduxHooks } from 'hooks';
import { ListPageSize, SortKeys } from 'data/constants/app'; import { ListPageSize, SortKeys } from 'data/constants/app';
@@ -15,8 +13,10 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('query-string', () => ({ const mockGet = jest.fn(() => ({}));
parse: jest.fn(() => ({})),
global.URLSearchParams = jest.fn().mockImplementation(() => ({
get: mockGet,
})); }));
const state = new MockUseState(hooks); const state = new MockUseState(hooks);
@@ -67,7 +67,7 @@ describe('CourseList hooks', () => {
it('loads current course list with page size 0 if/when there is query param disable_pagination=1', () => { it('loads current course list with page size 0 if/when there is query param disable_pagination=1', () => {
state.mock(); state.mock();
state.mockVal(state.keys.sortBy, testSortBy); state.mockVal(state.keys.sortBy, testSortBy);
queryString.parse.mockReturnValueOnce({ disable_pagination: 1 }); mockGet.mockReturnValueOnce('1');
out = hooks.useCourseListData(); out = hooks.useCourseListData();
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({ expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
sortBy: testSortBy, sortBy: testSortBy,

View File

@@ -1,6 +1,34 @@
import queryString from 'query-string';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
/**
* stringify(query, existingQuery)
* simple wrapper to convert an object to a query string
* @param {object} query - object to convert
* @param {string} existingQuery - existing query string
* @returns {string} - query string
*/
export const stringify = (query, existingQuery = '') => {
const searchParams = new URLSearchParams(existingQuery);
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') {
searchParams.delete(key);
} else if (Array.isArray(value)) {
searchParams.delete(key);
value.forEach((val) => {
if (val !== undefined && val !== null && val !== '') {
searchParams.append(key, val);
}
});
} else {
searchParams.set(key, value);
}
});
return searchParams.toString();
};
/** /**
* get(url) * get(url)
* simple wrapper providing an authenticated Http client get action * simple wrapper providing an authenticated Http client get action
@@ -10,21 +38,23 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
/** /**
* post(url, data) * post(url, data)
* simple wrapper providing an authenticated Http client post action * simple wrapper providing an authenticated Http client post action
* queryString.stringify is used to convert the object to query string with = and & * stringify is used to convert the object to query string with = and &
* @param {string} url - target url * @param {string} url - target url
* @param {object|string} body - post payload * @param {object|string} body - post payload
*/ */
export const post = (url, body) => getAuthenticatedHttpClient().post(url, queryString.stringify(body)); export const post = (url, body) => getAuthenticatedHttpClient().post(url, stringify(body));
export const client = getAuthenticatedHttpClient; export const client = getAuthenticatedHttpClient;
/** /**
* stringifyUrl(url, query) * stringifyUrl(url, query)
* simple wrapper around queryString.stringifyUrl that sets skip behavior * simple wrapper to convert a url and query object to a full url
* @param {string} url - base url string * @param {string} url - base url string
* @param {object} query - query parameters * @param {object} query - query parameters
* @returns {string} - full url
*/ */
export const stringifyUrl = (url, query) => queryString.stringifyUrl( export const stringifyUrl = (url, query) => {
{ url, query }, const [baseUrl, existingQuery = ''] = url.split('?');
{ skipNull: true, skipEmptyString: true }, const queryString = stringify(query, existingQuery);
); return queryString ? `${baseUrl}?${queryString}` : baseUrl;
};

View File

@@ -1,11 +1,6 @@
import queryString from 'query-string';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as utils from './utils'; import * as utils from './utils';
jest.mock('query-string', () => ({
stringifyUrl: jest.fn((url, options) => ({ url, options })),
stringify: jest.fn((data) => data),
}));
jest.mock('@edx/frontend-platform/auth', () => ({ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(), getAuthenticatedHttpClient: jest.fn(),
})); }));
@@ -20,28 +15,25 @@ describe('lms service utils', () => {
}); });
}); });
describe('post', () => { describe('post', () => {
it('forwards arguments to authenticatedHttpClient().post', () => { it('forwards arguments to authenticatedHttpClient().post, removes undefined attributes and appends array values', () => {
const post = jest.fn((...args) => ({ post: args })); const post = jest.fn((...args) => ({ post: args }));
getAuthenticatedHttpClient.mockReturnValue({ post }); getAuthenticatedHttpClient.mockReturnValue({ post });
const url = 'some url'; const url = 'some url';
const body = { const body = {
some: 'body', some: 'body',
for: 'the', for: undefined,
test: 'yay', test: 'yay',
array: ['one', 'two', undefined],
}; };
const expectedUrl = utils.post(url, body); const expectedUrl = utils.post(url, body);
expect(queryString.stringify).toHaveBeenCalledWith(body); expect(expectedUrl).toEqual(post(url, 'some=body&test=yay&array=one&array=two'));
expect(expectedUrl).toEqual(post(url, body));
}); });
}); });
describe('stringifyUrl', () => { describe('stringifyUrl', () => {
it('forwards url and query to stringifyUrl with options to skip null and ""', () => { it('forwards url and query to stringifyUrl skipping null and ""', () => {
const url = 'here.com'; const url = 'here.com';
const query = { some: 'set', of: 'queryParams' }; const query = { some: 'set', of: 'queryParams' };
const options = { skipNull: true, skipEmptyString: true }; expect(utils.stringifyUrl(url, query)).toEqual('here.com?some=set&of=queryParams');
expect(utils.stringifyUrl(url, query)).toEqual(
queryString.stringifyUrl({ url, query }, options),
);
}); });
}); });
}); });

View File

@@ -9,7 +9,7 @@ config.resolve.modules = [
'node_modules', 'node_modules',
]; ];
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/; config.module.rules[0].exclude = /node_modules\/(?!(split-on-first|strict-uri-encode|@edx))/;
config.plugins.push( config.plugins.push(
new CopyPlugin({ new CopyPlugin({

View File

@@ -9,7 +9,7 @@ config.resolve.modules = [
'node_modules', 'node_modules',
]; ];
config.module.rules[0].exclude = /node_modules\/(?!(query-string|split-on-first|strict-uri-encode|@edx))/; config.module.rules[0].exclude = /node_modules\/(?!(split-on-first|strict-uri-encode|@edx))/;
config.plugins.push( config.plugins.push(
new CopyPlugin({ new CopyPlugin({