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",
"moment": "^2.29.4",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
@@ -16469,24 +16468,6 @@
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",

View File

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

View File

@@ -1,7 +1,5 @@
import React from 'react';
import queryString from 'query-string';
import { ListPageSize, SortKeys } from 'data/constants/app';
import { reduxHooks } from 'hooks';
import { StrictDict } from 'utils';
@@ -27,12 +25,13 @@ export const useCourseListData = () => {
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({
sortBy,
filters,
pageSize: querySearch?.disable_pagination === 1 ? 0 : ListPageSize,
pageSize: Number(disablePagination) === 1 ? 0 : ListPageSize,
});
const handleRemoveFilter = (filter) => () => removeFilter(filter);

View File

@@ -1,5 +1,3 @@
import queryString from 'query-string';
import { MockUseState } from 'testUtils';
import { reduxHooks } from 'hooks';
import { ListPageSize, SortKeys } from 'data/constants/app';
@@ -15,8 +13,10 @@ jest.mock('hooks', () => ({
},
}));
jest.mock('query-string', () => ({
parse: jest.fn(() => ({})),
const mockGet = jest.fn(() => ({}));
global.URLSearchParams = jest.fn().mockImplementation(() => ({
get: mockGet,
}));
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', () => {
state.mock();
state.mockVal(state.keys.sortBy, testSortBy);
queryString.parse.mockReturnValueOnce({ disable_pagination: 1 });
mockGet.mockReturnValueOnce('1');
out = hooks.useCourseListData();
expect(reduxHooks.useCurrentCourseList).toHaveBeenCalledWith({
sortBy: testSortBy,

View File

@@ -1,6 +1,34 @@
import queryString from 'query-string';
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)
* simple wrapper providing an authenticated Http client get action
@@ -10,21 +38,23 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
/**
* post(url, data)
* 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 {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;
/**
* 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 {object} query - query parameters
* @returns {string} - full url
*/
export const stringifyUrl = (url, query) => queryString.stringifyUrl(
{ url, query },
{ skipNull: true, skipEmptyString: true },
);
export const stringifyUrl = (url, query) => {
const [baseUrl, existingQuery = ''] = url.split('?');
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 * 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', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
@@ -20,28 +15,25 @@ describe('lms service utils', () => {
});
});
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 }));
getAuthenticatedHttpClient.mockReturnValue({ post });
const url = 'some url';
const body = {
some: 'body',
for: 'the',
for: undefined,
test: 'yay',
array: ['one', 'two', undefined],
};
const expectedUrl = utils.post(url, body);
expect(queryString.stringify).toHaveBeenCalledWith(body);
expect(expectedUrl).toEqual(post(url, body));
expect(expectedUrl).toEqual(post(url, 'some=body&test=yay&array=one&array=two'));
});
});
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 query = { some: 'set', of: 'queryParams' };
const options = { skipNull: true, skipEmptyString: true };
expect(utils.stringifyUrl(url, query)).toEqual(
queryString.stringifyUrl({ url, query }, options),
);
expect(utils.stringifyUrl(url, query)).toEqual('here.com?some=set&of=queryParams');
});
});
});

View File

@@ -9,7 +9,7 @@ config.resolve.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(
new CopyPlugin({

View File

@@ -9,7 +9,7 @@ config.resolve.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(
new CopyPlugin({