feat: extends useSearchParams hook (#5203)

This commit is contained in:
Jarrod Flesch
2024-02-28 09:33:59 -05:00
committed by GitHub
parent 96349be350
commit 170ae3602b
18 changed files with 147 additions and 69 deletions

View File

@@ -7,12 +7,12 @@ import type { Props } from './types'
import Form from '../../forms/Form' import Form from '../../forms/Form'
import { useForm } from '../../forms/Form/context' import { useForm } from '../../forms/Form/context'
import RenderFields from '../../forms/RenderFields'
import FormSubmit from '../../forms/Submit' import FormSubmit from '../../forms/Submit'
import { X } from '../../icons/X' import { X } from '../../icons/X'
import { useAuth } from '../../providers/Auth' import { useAuth } from '../../providers/Auth'
import { useConfig } from '../../providers/Config' import { useConfig } from '../../providers/Config'
import { OperationContext } from '../../providers/OperationProvider' import { OperationContext } from '../../providers/OperationProvider'
import { useSearchParams } from '../../providers/SearchParams'
import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider' import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider'
import { useTranslation } from '../../providers/Translation' import { useTranslation } from '../../providers/Translation'
import { Drawer, DrawerToggler } from '../Drawer' import { Drawer, DrawerToggler } from '../Drawer'
@@ -82,7 +82,7 @@ const SaveDraft: React.FC<{ action: string; disabled: boolean }> = ({ action, di
) )
} }
const EditMany: React.FC<Props> = (props) => { const EditMany: React.FC<Props> = (props) => {
const { collection: { fields, labels: { plural }, slug } = {}, collection, resetParams } = props const { collection: { slug, fields, labels: { plural } } = {}, collection } = props
const { permissions } = useAuth() const { permissions } = useAuth()
const { closeModal } = useModal() const { closeModal } = useModal()
@@ -93,6 +93,7 @@ const EditMany: React.FC<Props> = (props) => {
const { count, getQueryParams, selectAll } = useSelection() const { count, getQueryParams, selectAll } = useSelection()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [selected, setSelected] = useState([]) const [selected, setSelected] = useState([])
const { dispatchSearchParams } = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
const hasUpdatePermission = collectionPermissions?.update?.permission const hasUpdatePermission = collectionPermissions?.update?.permission
@@ -104,7 +105,11 @@ const EditMany: React.FC<Props> = (props) => {
} }
const onSuccess = () => { const onSuccess = () => {
resetParams({ page: selectAll === SelectAllStatus.AllAvailable ? 1 : undefined }) dispatchSearchParams({
type: 'set',
browserHistory: 'replace',
params: { page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined },
})
} }
return ( return (

View File

@@ -2,5 +2,4 @@ import type { SanitizedCollectionConfig } from 'payload/types'
export type Props = { export type Props = {
collection: SanitizedCollectionConfig collection: SanitizedCollectionConfig
resetParams: () => void
} }

View File

@@ -40,12 +40,11 @@ export const ListControls: React.FC<Props> = (props) => {
handleSortChange, handleSortChange,
handleWhereChange, handleWhereChange,
modifySearchQuery = true, modifySearchQuery = true,
resetParams,
textFieldsToBeSearched, textFieldsToBeSearched,
titleField, titleField,
} = props } = props
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const shouldInitializeWhereOpened = validateWhereQuery(searchParams?.where) const shouldInitializeWhereOpened = validateWhereQuery(searchParams?.where)
const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>( const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>(

View File

@@ -11,7 +11,6 @@ export type Props = {
handleSortChange?: (sort: string) => void handleSortChange?: (sort: string) => void
handleWhereChange?: (where: Where) => void handleWhereChange?: (where: Where) => void
modifySearchQuery?: boolean modifySearchQuery?: boolean
resetParams?: () => void
textFieldsToBeSearched?: FieldAffectingData[] textFieldsToBeSearched?: FieldAffectingData[]
titleField: FieldAffectingData titleField: FieldAffectingData
} }

View File

@@ -22,7 +22,7 @@ const Localizer: React.FC<{
const { i18n } = useTranslation() const { i18n } = useTranslation()
const locale = useLocale() const locale = useLocale()
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const localeLabel = getTranslation(locale.label, i18n) const localeLabel = getTranslation(locale.label, i18n)

View File

@@ -21,7 +21,7 @@ const baseClass = 'paginator'
export const Pagination: React.FC<Props> = (props) => { export const Pagination: React.FC<Props> = (props) => {
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const pathname = usePathname() const pathname = usePathname()
const { const {

View File

@@ -30,7 +30,7 @@ export const PerPage: React.FC<Props> = ({
modifySearchParams = true, modifySearchParams = true,
resetPage = false, resetPage = false,
}) => { }) => {
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const history = useHistory() const history = useHistory()
const { t } = useTranslation() const { t } = useTranslation()

View File

@@ -8,10 +8,11 @@ import type { Props } from './types'
import { useAuth } from '../../providers/Auth' import { useAuth } from '../../providers/Auth'
import { useConfig } from '../../providers/Config' import { useConfig } from '../../providers/Config'
import { useSearchParams } from '../../providers/SearchParams'
import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider' import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider'
import { useTranslation } from '../../providers/Translation' import { useTranslation } from '../../providers/Translation'
// import { requests } from '../../../api'
import { MinimalTemplate } from '../../templates/Minimal' import { MinimalTemplate } from '../../templates/Minimal'
import { requests } from '../../utilities/api'
import { Button } from '../Button' import { Button } from '../Button'
import Pill from '../Pill' import Pill from '../Pill'
import './index.scss' import './index.scss'
@@ -19,7 +20,7 @@ import './index.scss'
const baseClass = 'publish-many' const baseClass = 'publish-many'
const PublishMany: React.FC<Props> = (props) => { const PublishMany: React.FC<Props> = (props) => {
const { collection: { labels: { plural }, slug, versions } = {}, resetParams } = props const { collection: { slug, labels: { plural }, versions } = {} } = props
const { const {
routes: { api }, routes: { api },
@@ -28,8 +29,9 @@ const PublishMany: React.FC<Props> = (props) => {
const { permissions } = useAuth() const { permissions } = useAuth()
const { toggleModal } = useModal() const { toggleModal } = useModal()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const { count, getQueryParams, selectAll } = useSelection() const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
const { dispatchSearchParams } = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
const hasPermission = collectionPermissions?.update?.permission const hasPermission = collectionPermissions?.update?.permission
@@ -40,53 +42,57 @@ const PublishMany: React.FC<Props> = (props) => {
toast.error(t('error:unknown')) toast.error(t('error:unknown'))
}, [t]) }, [t])
const handlePublish = useCallback(() => { const handlePublish = useCallback(async () => {
setSubmitted(true) setSubmitted(true)
// requests await requests
// .patch( .patch(
// `${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}`, `${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}`,
// { {
// body: JSON.stringify({ body: JSON.stringify({
// _status: 'published', _status: 'published',
// }), }),
// headers: { headers: {
// 'Accept-Language': i18n.language, 'Accept-Language': i18n.language,
// 'Content-Type': 'application/json', 'Content-Type': 'application/json',
// }, },
// }, },
// ) )
// .then(async (res) => { .then(async (res) => {
// try { try {
// const json = await res.json() const json = await res.json()
// toggleModal(modalSlug) toggleModal(modalSlug)
// if (res.status < 400) { if (res.status < 400) {
// toast.success(t('general:updatedSuccessfully')) toast.success(t('general:updatedSuccessfully'))
// resetParams({ page: selectAll ? 1 : undefined }) dispatchSearchParams({
// return null type: 'set',
// } browserHistory: 'replace',
params: { page: selectAll ? '1' : undefined },
})
return null
}
// if (json.errors) { if (json.errors) {
// json.errors.forEach((error) => toast.error(error.message)) json.errors.forEach((error) => toast.error(error.message))
// } else { } else {
// addDefaultError() addDefaultError()
// } }
// return false return false
// } catch (e) { } catch (e) {
// return addDefaultError() return addDefaultError()
// } }
// }) })
}, [ }, [
addDefaultError, addDefaultError,
api, api,
getQueryParams, getQueryParams,
i18n.language, i18n.language,
modalSlug, modalSlug,
resetParams,
selectAll, selectAll,
serverURL, serverURL,
slug, slug,
t, t,
toggleModal, toggleModal,
dispatchSearchParams,
]) ])
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) { if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {

View File

@@ -2,5 +2,4 @@ import type { SanitizedCollectionConfig } from 'payload/types'
export type Props = { export type Props = {
collection: SanitizedCollectionConfig collection: SanitizedCollectionConfig
resetParams: () => void
} }

View File

@@ -22,7 +22,7 @@ const SearchFilter: React.FC<Props> = (props) => {
modifySearchQuery = true, modifySearchQuery = true,
} = props } = props
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const history = useHistory() const history = useHistory()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()

View File

@@ -15,7 +15,7 @@ const baseClass = 'sort-column'
export const SortColumn: React.FC<Props> = (props) => { export const SortColumn: React.FC<Props> = (props) => {
const { name, disable = false, label } = props const { name, disable = false, label } = props
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const history = useHistory() const history = useHistory()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()

View File

@@ -20,7 +20,7 @@ const SortComplex: React.FC<Props> = (props) => {
const { collection, handleChange, modifySearchQuery = true } = props const { collection, handleChange, modifySearchQuery = true } = props
const history = useHistory() const history = useHistory()
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [sortOptions, setSortOptions] = useState<OptionObject[]>() const [sortOptions, setSortOptions] = useState<OptionObject[]>()

View File

@@ -66,7 +66,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
const collection = config.collections.find((c) => c.slug === collectionSlug) const collection = config.collections.find((c) => c.slug === collectionSlug)
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n)) const [reducedFields] = useState(() => reduceFields(collection.fields, i18n))
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
// This handles initializing the where conditions from the search query (URL). That way, if you pass in // This handles initializing the where conditions from the search query (URL). That way, if you pass in
// query params to the URL, the where conditions will be initialized from those and displayed in the UI. // query params to the URL, the where conditions will be initialized from those and displayed in the UI.
@@ -187,7 +187,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border" iconStyle="with-border"
onClick={() => { onClick={() => {
if (reducedFields.length > 0) if (reducedFields.length > 0)
dispatchConditions({ field: reducedFields[0].value, type: 'add' }) dispatchConditions({ type: 'add', field: reducedFields[0].value })
}} }}
> >
{t('general:or')} {t('general:or')}
@@ -205,7 +205,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
iconStyle="with-border" iconStyle="with-border"
onClick={() => { onClick={() => {
if (reducedFields.length > 0) if (reducedFields.length > 0)
dispatchConditions({ field: reducedFields[0].value, type: 'add' }) dispatchConditions({ type: 'add', field: reducedFields[0].value })
}} }}
> >
{t('general:addFilter')} {t('general:addFilter')}

View File

@@ -2,7 +2,7 @@
import type { Permissions, User } from 'payload/auth' import type { Permissions, User } from 'payload/auth'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import qs from 'qs' import qs from 'qs'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
@@ -13,6 +13,7 @@ import useDebounce from '../../hooks/useDebounce'
import { useTranslation } from '../../providers/Translation' import { useTranslation } from '../../providers/Translation'
import { requests } from '../../utilities/api' import { requests } from '../../utilities/api'
import { useConfig } from '../Config' import { useConfig } from '../Config'
import { useSearchParams } from '../SearchParams'
// import { useLocale } from '../Locale' // import { useLocale } from '../Locale'
const Context = createContext({} as AuthContext) const Context = createContext({} as AuthContext)
@@ -20,7 +21,7 @@ const Context = createContext({} as AuthContext)
const maxTimeoutTime = 2147483647 const maxTimeoutTime = 2147483647
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const [user, setUser] = useState<User | null>() const [user, setUser] = useState<User | null>()
const [tokenInMemory, setTokenInMemory] = useState<string>() const [tokenInMemory, setTokenInMemory] = useState<string>()
const [tokenExpiration, setTokenExpiration] = useState<number>() const [tokenExpiration, setTokenExpiration] = useState<number>()

View File

@@ -20,7 +20,7 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
const defaultLocale = const defaultLocale =
localization && localization.defaultLocale ? localization.defaultLocale : 'en' localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const searchParams = useSearchParams() const { searchParams } = useSearchParams()
const [localeCode, setLocaleCode] = useState<string>( const [localeCode, setLocaleCode] = useState<string>(
(searchParams?.locale as string) || defaultLocale, (searchParams?.locale as string) || defaultLocale,

View File

@@ -1,17 +1,59 @@
'use client' 'use client'
import { useSearchParams as useNextSearchParams } from 'next/navigation' import { useSearchParams as useNextSearchParams, useRouter } from 'next/navigation'
import qs from 'qs' import qs from 'qs'
import React, { createContext, useContext } from 'react' import React, { createContext, useContext } from 'react'
interface ISearchParamsContext extends qs.ParsedQs {} import type { Action, SearchParamsContext, State } from './types'
const Context = createContext<ISearchParamsContext>({} as ISearchParamsContext) const initialContext: SearchParamsContext = {
dispatchSearchParams: () => {},
searchParams: {},
}
const Context = createContext(initialContext)
// TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts // TODO: abstract the `next/navigation` dependency out from this provider so that it can be used in other contexts
export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => { export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const nextSearchParams = useNextSearchParams() const nextSearchParams = useNextSearchParams()
const searchParams = qs.parse(nextSearchParams.toString(), { depth: 10, ignoreQueryPrefix: true }) const router = useRouter()
return <Context.Provider value={searchParams}>{children}</Context.Provider> const initialParams = qs.parse(nextSearchParams.toString(), {
depth: 10,
ignoreQueryPrefix: true,
})
const [searchParams, dispatchSearchParams] = React.useReducer((state: State, action: Action) => {
const stackAction = action.browserHistory || 'push'
let paramsToSet
switch (action.type) {
case 'set':
paramsToSet = {
...state,
...action.params,
}
break
case 'replace':
paramsToSet = action.params
break
case 'clear':
paramsToSet = {}
break
default:
return state
}
const newSearchString = qs.stringify(paramsToSet, { addQueryPrefix: true })
if (stackAction === 'push') {
router.push(newSearchString)
} else if (stackAction === 'replace') {
router.replace(newSearchString)
}
return paramsToSet
}, initialParams)
return (
<Context.Provider value={{ dispatchSearchParams, searchParams }}>{children}</Context.Provider>
)
} }
export const useSearchParams = (): ISearchParamsContext => useContext(Context) export const useSearchParams = (): SearchParamsContext => useContext(Context)

View File

@@ -0,0 +1,27 @@
export type SearchParamsContext = {
dispatchSearchParams: (action: Action) => void
searchParams: qs.ParsedQs
}
export type State = qs.ParsedQs
export type Action = (
| {
params: qs.ParsedQs
type: 'replace'
}
| {
params: qs.ParsedQs
type: 'set'
}
| {
type: 'clear'
}
) & {
/**
* `push` will add a new entry to the browser history stack.
* `replace` will overwrite the browser history entry.
* @default 'push'
* */
browserHistory?: 'push' | 'replace'
}

View File

@@ -5,6 +5,7 @@ import queryString from 'qs'
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { useLocale } from '../Locale' import { useLocale } from '../Locale'
import { useSearchParams } from '../SearchParams'
export enum SelectAllStatus { export enum SelectAllStatus {
AllAvailable = 'allAvailable', AllAvailable = 'allAvailable',
@@ -37,6 +38,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
const [selected, setSelected] = useState<SelectionContext['selected']>({}) const [selected, setSelected] = useState<SelectionContext['selected']>({})
const [selectAll, setSelectAll] = useState<SelectAllStatus>(SelectAllStatus.None) const [selectAll, setSelectAll] = useState<SelectAllStatus>(SelectAllStatus.None)
const [count, setCount] = useState(0) const [count, setCount] = useState(0)
const { searchParams } = useSearchParams()
const toggleAll = useCallback( const toggleAll = useCallback(
(allAvailable = false) => { (allAvailable = false) => {
@@ -83,11 +85,10 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
(additionalParams?: Where): string => { (additionalParams?: Where): string => {
let where: Where let where: Where
if (selectAll === SelectAllStatus.AllAvailable) { if (selectAll === SelectAllStatus.AllAvailable) {
// const params = queryString.parse(history.location.search, { ignoreQueryPrefix: true }) const params = searchParams?.where as Where
// .where as Where where = params || {
// where = params || { id: { not_equals: '' },
// id: { not_equals: '' }, }
// }
} else { } else {
where = { where = {
id: { id: {
@@ -110,7 +111,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
{ addQueryPrefix: true }, { addQueryPrefix: true },
) )
}, },
[selectAll, selected, locale], [selectAll, selected, locale, searchParams],
) )
useEffect(() => { useEffect(() => {