diff --git a/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx b/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx index 19fc84bfb7f..20e117e7312 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx @@ -1,10 +1,9 @@ 'use client' import type { SanitizedConfig } from 'payload' -import { useSearchParams } from '@payloadcms/ui' import { formatAdminURL } from '@payloadcms/ui/shared' import LinkImport from 'next/link.js' -import { useParams, usePathname } from 'next/navigation.js' +import { useParams, usePathname, useSearchParams } from 'next/navigation.js' import React from 'react' const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default @@ -30,12 +29,9 @@ export const DocumentTabLink: React.FC<{ const pathname = usePathname() const params = useParams() - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() - const locale = - 'locale' in searchParams && typeof searchParams.locale === 'string' - ? searchParams.locale - : undefined + const locale = searchParams.get('locale') const [entityType, entitySlug, segmentThree, segmentFour, ...rest] = params.segments || [] const isCollection = entityType === 'collections' diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index c66b47cb8f3..c37d905508e 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -3,14 +3,14 @@ import type { ClientCollectionConfig } from 'payload' import { Modal, useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' +import { useRouter, useSearchParams } from 'next/navigation.js' +import * as qs from 'qs-esm' import React, { useCallback, useState } from 'react' import { toast } from 'sonner' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' -import { useSearchParams } from '../../providers/SearchParams/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' @@ -41,7 +41,7 @@ export const DeleteMany: React.FC = (props) => { const { i18n, t } = useTranslation() const [deleting, setDeleting] = useState(false) const router = useRouter() - const { searchParams, stringifyParams } = useSearchParams() + const searchParams = useSearchParams() const { clearRouteCache } = useRouteCache() const collectionPermissions = permissions?.collections?.[slug] @@ -58,7 +58,7 @@ export const DeleteMany: React.FC = (props) => { const queryWithSearch = mergeListSearchAndWhere({ collectionConfig: collection, - search: searchParams?.search as string, + search: searchParams.get('search'), }) const queryString = getQueryParams(queryWithSearch) @@ -85,21 +85,26 @@ export const DeleteMany: React.FC = (props) => { label: getTranslation(successLabel, i18n), }), ) + if (json?.errors.length > 0) { toast.error(json.message, { description: json.errors.map((error) => error.message).join('\n'), }) } + toggleAll() + router.replace( - stringifyParams({ - params: { + qs.stringify( + { page: selectAll ? '1' : undefined, }, - replace: true, - }), + { addQueryPrefix: true }, + ), ) + clearRouteCache() + return null } @@ -111,7 +116,7 @@ export const DeleteMany: React.FC = (props) => { addDefaultError() } return false - } catch (e) { + } catch (_err) { return addDefaultError() } }) @@ -128,7 +133,6 @@ export const DeleteMany: React.FC = (props) => { serverURL, singular, slug, - stringifyParams, t, toggleAll, toggleModal, diff --git a/packages/ui/src/elements/EditMany/index.tsx b/packages/ui/src/elements/EditMany/index.tsx index f2c82c8cf32..8f4b64ab423 100644 --- a/packages/ui/src/elements/EditMany/index.tsx +++ b/packages/ui/src/elements/EditMany/index.tsx @@ -3,7 +3,8 @@ import type { ClientCollectionConfig, FormState } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' +import { useRouter, useSearchParams } from 'next/navigation.js' +import * as qs from 'qs-esm' import React, { useCallback, useEffect, useMemo, useState } from 'react' import type { FormProps } from '../../forms/Form/index.js' @@ -19,14 +20,14 @@ import { DocumentInfoProvider } from '../../providers/DocumentInfo/index.js' import { EditDepthProvider } from '../../providers/EditDepth/index.js' import { OperationContext } from '../../providers/Operation/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' -import { useSearchParams } from '../../providers/SearchParams/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js' -import { Drawer, DrawerToggler } from '../Drawer/index.js' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' import './index.scss' +import { Drawer, DrawerToggler } from '../Drawer/index.js' import { FieldSelect } from '../FieldSelect/index.js' const baseClass = 'edit-many' @@ -125,7 +126,7 @@ export const EditMany: React.FC = (props) => { const { count, getQueryParams, selectAll } = useSelection() const { i18n, t } = useTranslation() const [selected, setSelected] = useState([]) - const { searchParams, stringifyParams } = useSearchParams() + const searchParams = useSearchParams() const router = useRouter() const [initialState, setInitialState] = useState() const hasInitializedState = React.useRef(false) @@ -195,7 +196,7 @@ export const EditMany: React.FC = (props) => { const queryString = useMemo(() => { const queryWithSearch = mergeListSearchAndWhere({ collectionConfig: collection, - search: searchParams?.search as string, + search: searchParams.get('search'), }) return getQueryParams(queryWithSearch) @@ -203,9 +204,13 @@ export const EditMany: React.FC = (props) => { const onSuccess = () => { router.replace( - stringifyParams({ - params: { page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined }, - }), + qs.stringify( + { + ...parseSearchParams(searchParams), + page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined, + }, + { addQueryPrefix: true }, + ), ) clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this closeModal(drawerSlug) diff --git a/packages/ui/src/elements/Localizer/index.tsx b/packages/ui/src/elements/Localizer/index.tsx index 26395c52711..5ed3790e81a 100644 --- a/packages/ui/src/elements/Localizer/index.tsx +++ b/packages/ui/src/elements/Localizer/index.tsx @@ -1,14 +1,16 @@ 'use client' import { getTranslation } from '@payloadcms/translations' +import { useSearchParams } from 'next/navigation.js' +import * as qs from 'qs-esm' import React from 'react' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' -import { useSearchParams } from '../../providers/SearchParams/index.js' import { useTranslation } from '../../providers/Translation/index.js' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { Popup, PopupList } from '../Popup/index.js' -import './index.scss' import { LocalizerLabel } from './LocalizerLabel/index.js' +import './index.scss' const baseClass = 'localizer' @@ -18,10 +20,10 @@ export const Localizer: React.FC<{ const { className } = props const { config } = useConfig() const { localization } = config + const searchParams = useSearchParams() const { i18n } = useTranslation() const locale = useLocale() - const { stringifyParams } = useSearchParams() if (localization) { const { locales } = localization @@ -39,11 +41,13 @@ export const Localizer: React.FC<{ return ( diff --git a/packages/ui/src/elements/PublishMany/index.tsx b/packages/ui/src/elements/PublishMany/index.tsx index 0e532fe0bb9..96d685da405 100644 --- a/packages/ui/src/elements/PublishMany/index.tsx +++ b/packages/ui/src/elements/PublishMany/index.tsx @@ -3,17 +3,18 @@ import type { ClientCollectionConfig } from 'payload' import { Modal, useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' +import { useRouter, useSearchParams } from 'next/navigation.js' +import * as qs from 'qs-esm' import React, { useCallback, useState } from 'react' import { toast } from 'sonner' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' -import { useSearchParams } from '../../providers/SearchParams/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { Button } from '../Button/index.js' import { Pill } from '../Pill/index.js' import './index.scss' @@ -41,7 +42,7 @@ export const PublishMany: React.FC = (props) => { const { getQueryParams, selectAll } = useSelection() const [submitted, setSubmitted] = useState(false) const router = useRouter() - const { stringifyParams } = useSearchParams() + const searchParams = useSearchParams() const collectionPermissions = permissions?.collections?.[slug] const hasPermission = collectionPermissions?.update @@ -82,17 +83,21 @@ export const PublishMany: React.FC = (props) => { label: getTranslation(successLabel, i18n), }), ) + if (json?.errors.length > 0) { toast.error(json.message, { description: json.errors.map((error) => error.message).join('\n'), }) } + router.replace( - stringifyParams({ - params: { + qs.stringify( + { + ...parseSearchParams(searchParams), page: selectAll ? '1' : undefined, }, - }), + { addQueryPrefix: true }, + ), ) clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this @@ -110,21 +115,21 @@ export const PublishMany: React.FC = (props) => { } }) }, [ - addDefaultError, + serverURL, api, + slug, getQueryParams, i18n, + toggleModal, modalSlug, plural, - selectAll, - serverURL, singular, - slug, t, - toggleModal, router, - stringifyParams, + searchParams, + selectAll, clearRouteCache, + addDefaultError, ]) if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { diff --git a/packages/ui/src/elements/SortComplex/index.tsx b/packages/ui/src/elements/SortComplex/index.tsx index 67fb737ecad..4dc38d4b3ae 100644 --- a/packages/ui/src/elements/SortComplex/index.tsx +++ b/packages/ui/src/elements/SortComplex/index.tsx @@ -3,7 +3,7 @@ import type { OptionObject, SanitizedCollectionConfig } from 'payload' import { getTranslation } from '@payloadcms/translations' // TODO: abstract the `next/navigation` dependency out from this component -import { usePathname, useRouter } from 'next/navigation.js' +import { usePathname, useRouter, useSearchParams } from 'next/navigation.js' import { sortableFieldTypes } from 'payload' import { fieldAffectsData } from 'payload/shared' import * as qs from 'qs-esm' @@ -18,7 +18,6 @@ export type SortComplexProps = { import type { Option } from '../ReactSelect/index.js' -import { useSearchParams } from '../../providers/SearchParams/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { ReactSelect } from '../ReactSelect/index.js' import './index.scss' @@ -30,7 +29,7 @@ export const SortComplex: React.FC = (props) => { const router = useRouter() const pathname = usePathname() - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const { i18n, t } = useTranslation() const [sortOptions, setSortOptions] = useState() @@ -58,7 +57,7 @@ export const SortComplex: React.FC = (props) => { handleChange(newSortValue) } - if (searchParams.sort !== newSortValue && modifySearchQuery) { + if (searchParams.get('sort') !== newSortValue && modifySearchQuery) { const search = qs.stringify( { ...searchParams, diff --git a/packages/ui/src/elements/UnpublishMany/index.tsx b/packages/ui/src/elements/UnpublishMany/index.tsx index 27428213c31..d74f58e7878 100644 --- a/packages/ui/src/elements/UnpublishMany/index.tsx +++ b/packages/ui/src/elements/UnpublishMany/index.tsx @@ -1,13 +1,13 @@ 'use client' import { Modal, useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' +import { useRouter, useSearchParams } from 'next/navigation.js' +import * as qs from 'qs-esm' import React, { useCallback, useState } from 'react' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useRouteCache } from '../../providers/RouteCache/index.js' -import { useSearchParams } from '../../providers/SearchParams/index.js' import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' @@ -21,6 +21,8 @@ import type { ClientCollectionConfig } from 'payload' import { toast } from 'sonner' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' + export type UnpublishManyProps = { collection: ClientCollectionConfig } @@ -38,9 +40,9 @@ export const UnpublishMany: React.FC = (props) => { const { permissions } = useAuth() const { toggleModal } = useModal() const { i18n, t } = useTranslation() + const searchParams = useSearchParams() const { getQueryParams, selectAll } = useSelection() const [submitted, setSubmitted] = useState(false) - const { stringifyParams } = useSearchParams() const router = useRouter() const { clearRouteCache } = useRouteCache() @@ -86,11 +88,13 @@ export const UnpublishMany: React.FC = (props) => { }) } router.replace( - stringifyParams({ - params: { + qs.stringify( + { + ...parseSearchParams(searchParams), page: selectAll ? '1' : undefined, }, - }), + { addQueryPrefix: true }, + ), ) clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this return null @@ -102,26 +106,26 @@ export const UnpublishMany: React.FC = (props) => { addDefaultError() } return false - } catch (e) { + } catch (_err) { return addDefaultError() } }) }, [ - addDefaultError, + serverURL, api, + slug, getQueryParams, i18n, + toggleModal, modalSlug, plural, - selectAll, - serverURL, singular, - slug, t, - toggleModal, router, + searchParams, + selectAll, clearRouteCache, - stringifyParams, + addDefaultError, ]) if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { diff --git a/packages/ui/src/providers/ListQuery/index.tsx b/packages/ui/src/providers/ListQuery/index.tsx index 62bbc66a6d2..9bebb27da8c 100644 --- a/packages/ui/src/providers/ListQuery/index.tsx +++ b/packages/ui/src/providers/ListQuery/index.tsx @@ -9,8 +9,8 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS import type { Column } from '../../elements/Table/index.js' import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { usePreferences } from '../Preferences/index.js' -import { createParams } from '../SearchParams/index.js' export type ColumnPreferences = Pick[] @@ -59,7 +59,7 @@ export const ListQueryProvider: React.FC = ({ const router = useRouter() const { setPreference } = usePreferences() const rawSearchParams = useSearchParams() - const searchParams = useMemo(() => createParams(rawSearchParams), [rawSearchParams]) + const searchParams = useMemo(() => parseSearchParams(rawSearchParams), [rawSearchParams]) const { onQueryChange } = useListDrawerContext() diff --git a/packages/ui/src/providers/Locale/index.tsx b/packages/ui/src/providers/Locale/index.tsx index 6248933cc2e..1dccc02d45e 100644 --- a/packages/ui/src/providers/Locale/index.tsx +++ b/packages/ui/src/providers/Locale/index.tsx @@ -2,13 +2,13 @@ import type { Locale } from 'payload' +import { useSearchParams } from 'next/navigation.js' import React, { createContext, useContext, useEffect, useState } from 'react' import { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js' import { useAuth } from '../Auth/index.js' import { useConfig } from '../Config/index.js' import { usePreferences } from '../Preferences/index.js' -import { useSearchParams } from '../SearchParams/index.js' const LocaleContext = createContext({} as Locale) @@ -21,12 +21,10 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child const defaultLocale = localization && localization.defaultLocale ? localization.defaultLocale : 'en' - const { searchParams } = useSearchParams() - const localeFromParams = searchParams?.locale + const searchParams = useSearchParams() + const localeFromParams = searchParams.get('locale') - const [localeCode, setLocaleCode] = useState( - (localeFromParams as string) || defaultLocale, - ) + const [localeCode, setLocaleCode] = useState(localeFromParams || defaultLocale) const [locale, setLocale] = useState( localization && findLocaleFromCode(localization, localeCode), diff --git a/packages/ui/src/providers/Params/index.tsx b/packages/ui/src/providers/Params/index.tsx index f3bd64b7a6e..bfbcb746444 100644 --- a/packages/ui/src/providers/Params/index.tsx +++ b/packages/ui/src/providers/Params/index.tsx @@ -8,10 +8,25 @@ interface IParamsContext extends Params {} const Context = createContext({} as IParamsContext) -// TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts +/** + * @deprecated + * The ParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. + * @example + * ```tsx + * import { useParams } from 'next/navigation' + * ``` + */ export const ParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const params = useNextParams() return {children} } +/** + * @deprecated + * The `useParams` hook is deprecated and will be removed in the next major release. Instead, use the `useParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. + * @example + * ```tsx + * import { useParams } from 'next/navigation' + * ``` + */ export const useParams = (): IParamsContext => useContext(Context) diff --git a/packages/ui/src/providers/SearchParams/index.tsx b/packages/ui/src/providers/SearchParams/index.tsx index 83f62597e8c..5a14b1d3f3d 100644 --- a/packages/ui/src/providers/SearchParams/index.tsx +++ b/packages/ui/src/providers/SearchParams/index.tsx @@ -1,17 +1,16 @@ 'use client' -import type { ReadonlyURLSearchParams } from 'next/navigation.js' import { useSearchParams as useNextSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' import React, { createContext, useContext } from 'react' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' + export type SearchParamsContext = { searchParams: qs.ParsedQs - stringifyParams: ({ params, replace }: { params: State; replace?: boolean }) => string + stringifyParams: ({ params, replace }: { params: qs.ParsedQs; replace?: boolean }) => string } -export type State = qs.ParsedQs - const initialContext: SearchParamsContext = { searchParams: {}, stringifyParams: () => '', @@ -19,23 +18,21 @@ const initialContext: SearchParamsContext = { const Context = createContext(initialContext) -export function createParams(params: ReadonlyURLSearchParams): State { - const search = params.toString() - - return qs.parse(search, { - depth: 10, - ignoreQueryPrefix: true, - }) -} - -// TODO: this provider should likely be marked as deprecated and then removed in the next major release +/** + * @deprecated + * The SearchParamsProvider is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. + * @example + * ```tsx + * import { useSearchParams } from 'next/navigation' + * ``` + */ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { const nextSearchParams = useNextSearchParams() - const [searchParams, setSearchParams] = React.useState(() => createParams(nextSearchParams)) + const [searchParams, setSearchParams] = React.useState(() => parseSearchParams(nextSearchParams)) const stringifyParams = React.useCallback( - ({ params, replace = false }: { params: State; replace?: boolean }) => { + ({ params, replace = false }: { params: qs.ParsedQs; replace?: boolean }) => { return qs.stringify( { ...(replace ? {} : searchParams), @@ -48,10 +45,23 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ ) React.useEffect(() => { - setSearchParams(createParams(nextSearchParams)) + setSearchParams(parseSearchParams(nextSearchParams)) }, [nextSearchParams]) return {children} } +/** + * @deprecated + * The `useSearchParams` hook is deprecated and will be removed in the next major release. Instead, use the `useSearchParams` hook from `next/navigation` directly. See https://github.com/payloadcms/payload/pull/9581. + * @example + * ```tsx + * import { useSearchParams } from 'next/navigation' + * ``` + * If you need to parse the `where` query, you can do so with the `parseSearchParams` utility. + * ```tsx + * import { parseSearchParams } from '@payloadcms/ui' + * const parsedSearchParams = parseSearchParams(searchParams) + * ``` + */ export const useSearchParams = (): SearchParamsContext => useContext(Context) diff --git a/packages/ui/src/providers/Selection/index.tsx b/packages/ui/src/providers/Selection/index.tsx index 7d79cf188e8..51fbff5d078 100644 --- a/packages/ui/src/providers/Selection/index.tsx +++ b/packages/ui/src/providers/Selection/index.tsx @@ -1,11 +1,12 @@ 'use client' import type { ClientUser, Where } from 'payload' +import { useSearchParams } from 'next/navigation.js' import * as qs from 'qs-esm' import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' +import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { useLocale } from '../Locale/index.js' -import { useSearchParams } from '../SearchParams/index.js' export enum SelectAllStatus { AllAvailable = 'allAvailable', @@ -51,7 +52,7 @@ export const SelectionProvider: React.FC = ({ children, docs = [], totalD const [selectAll, setSelectAll] = useState(SelectAllStatus.None) const [count, setCount] = useState(0) - const { searchParams } = useSearchParams() + const searchParams = useSearchParams() const toggleAll = useCallback( (allAvailable = false) => { @@ -110,7 +111,7 @@ export const SelectionProvider: React.FC = ({ children, docs = [], totalD let where: Where if (selectAll === SelectAllStatus.AllAvailable) { - const params = searchParams?.where as Where + const params = parseSearchParams(searchParams)?.where as Where where = params || { id: { not_equals: '' }, diff --git a/packages/ui/src/utilities/parseSearchParams.ts b/packages/ui/src/utilities/parseSearchParams.ts new file mode 100644 index 00000000000..422985bd6ba --- /dev/null +++ b/packages/ui/src/utilities/parseSearchParams.ts @@ -0,0 +1,12 @@ +import type { ReadonlyURLSearchParams } from 'next/navigation.js' + +import * as qs from 'qs-esm' + +export function parseSearchParams(params: ReadonlyURLSearchParams): qs.ParsedQs { + const search = params.toString() + + return qs.parse(search, { + depth: 10, + ignoreQueryPrefix: true, + }) +}