diff --git a/package-lock.json b/package-lock.json index 025b884..4060b73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b54cdc5..f41a902 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/containers/CoursesPanel/hooks.js b/src/containers/CoursesPanel/hooks.js index dcc5a60..2cdeeb3 100644 --- a/src/containers/CoursesPanel/hooks.js +++ b/src/containers/CoursesPanel/hooks.js @@ -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); diff --git a/src/containers/CoursesPanel/hooks.test.js b/src/containers/CoursesPanel/hooks.test.js index 86789d1..64f608c 100644 --- a/src/containers/CoursesPanel/hooks.test.js +++ b/src/containers/CoursesPanel/hooks.test.js @@ -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, diff --git a/src/data/services/lms/utils.js b/src/data/services/lms/utils.js index 65a5ff6..f1c802d 100644 --- a/src/data/services/lms/utils.js +++ b/src/data/services/lms/utils.js @@ -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; +}; diff --git a/src/data/services/lms/utils.test.js b/src/data/services/lms/utils.test.js index fbb0a93..7d78a07 100644 --- a/src/data/services/lms/utils.test.js +++ b/src/data/services/lms/utils.test.js @@ -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'); }); }); }); diff --git a/webpack.dev.config.js b/webpack.dev.config.js index a7c11c2..b567676 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -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({ diff --git a/webpack.prod.config.js b/webpack.prod.config.js index 46c933b..9cf64fc 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -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({