fix(ui): various issues around documents lists, listQuery provider and search params (#8081)
This PR fixes and improves: - ListQuery provider is now the source of truth for searchParams instead of having components use the `useSearchParams` hook - Various issues with search params and filters sticking around when navigating between collections - Pagination and limits not working inside DocumentDrawer - Searching and filtering causing a flash of overlay in DocumentDrawer, this now only shows for the first load and on slow networks - Preferences are now respected in DocumentDrawer - Changing the limit now resets your page back to 1 in case the current page no longer exists Fixes https://github.com/payloadcms/payload/issues/7085 Fixes https://github.com/payloadcms/payload/pull/8081 Fixes https://github.com/payloadcms/payload/issues/8086
This commit is contained in:
@@ -28,7 +28,6 @@ import {
|
||||
useListQuery,
|
||||
useModal,
|
||||
useRouteCache,
|
||||
useSearchParams,
|
||||
useStepNav,
|
||||
useTranslation,
|
||||
useWindowInfo,
|
||||
@@ -54,8 +53,7 @@ export const DefaultListView: React.FC = () => {
|
||||
newDocumentURL,
|
||||
} = useListInfo()
|
||||
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange } = useListQuery()
|
||||
const { searchParams } = useSearchParams()
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange, params } = useListQuery()
|
||||
const { openModal } = useModal()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
const { setCollectionSlug, setOnSuccess } = useBulkUpload()
|
||||
@@ -226,9 +224,7 @@ export const DefaultListView: React.FC = () => {
|
||||
</div>
|
||||
<PerPage
|
||||
handleChange={(limit) => void handlePerPageChange(limit)}
|
||||
limit={
|
||||
isNumber(searchParams?.limit) ? Number(searchParams.limit) : defaultLimit
|
||||
}
|
||||
limit={isNumber(params?.limit) ? Number(params.limit) : defaultLimit}
|
||||
limits={collectionConfig?.admin?.pagination?.limits}
|
||||
resetPage={data.totalDocs <= data.pagingCounter}
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||
import { SearchIcon } from '../../icons/Search/index.js'
|
||||
import { useListInfo } from '../../providers/ListInfo/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { ColumnSelector } from '../ColumnSelector/index.js'
|
||||
import { DeleteMany } from '../DeleteMany/index.js'
|
||||
@@ -48,17 +47,13 @@ export type ListControlsProps = {
|
||||
export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
const { collectionConfig, enableColumns = true, enableSort = false, fields } = props
|
||||
|
||||
const { handleSearchChange } = useListQuery()
|
||||
const { handleSearchChange, params } = useListQuery()
|
||||
const { beforeActions, collectionSlug, disableBulkDelete, disableBulkEdit } = useListInfo()
|
||||
const { searchParams } = useSearchParams()
|
||||
const titleField = useUseTitleField(collectionConfig, fields)
|
||||
const { i18n, t } = useTranslation()
|
||||
const {
|
||||
breakpoints: { s: smallBreak },
|
||||
} = useWindowInfo()
|
||||
const [search, setSearch] = useState(
|
||||
typeof searchParams?.search === 'string' ? searchParams?.search : '',
|
||||
)
|
||||
|
||||
const searchLabel =
|
||||
(titleField &&
|
||||
@@ -81,21 +76,21 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
t('general:searchBy', { label: getTranslation(searchLabel, i18n) }),
|
||||
)
|
||||
|
||||
const hasWhereParam = useRef(Boolean(searchParams?.where))
|
||||
const hasWhereParam = useRef(Boolean(params?.where))
|
||||
|
||||
const shouldInitializeWhereOpened = validateWhereQuery(searchParams?.where)
|
||||
const shouldInitializeWhereOpened = validateWhereQuery(params?.where)
|
||||
const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>(
|
||||
shouldInitializeWhereOpened ? 'where' : undefined,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasWhereParam.current && !searchParams?.where) {
|
||||
if (hasWhereParam.current && !params?.where) {
|
||||
setVisibleDrawer(undefined)
|
||||
hasWhereParam.current = false
|
||||
} else if (searchParams?.where) {
|
||||
} else if (params?.where) {
|
||||
hasWhereParam.current = true
|
||||
}
|
||||
}, [setVisibleDrawer, searchParams?.where])
|
||||
}, [setVisibleDrawer, params?.where])
|
||||
|
||||
useEffect(() => {
|
||||
if (listSearchableFields?.length > 0) {
|
||||
@@ -134,11 +129,10 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
handleChange={(search) => {
|
||||
return void handleSearchChange(search)
|
||||
}}
|
||||
initialParams={searchParams}
|
||||
// @ts-expect-error @todo: fix types
|
||||
initialParams={params}
|
||||
key={collectionSlug}
|
||||
label={searchLabelTranslated.current}
|
||||
setValue={setSearch}
|
||||
value={search}
|
||||
/>
|
||||
<div className={`${baseClass}__buttons`}>
|
||||
<div className={`${baseClass}__buttons-wrap`}>
|
||||
@@ -216,7 +210,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
collectionPluralLabel={collectionConfig?.labels?.plural}
|
||||
collectionSlug={collectionConfig.slug}
|
||||
fields={fields}
|
||||
key={String(hasWhereParam.current && !searchParams?.where)}
|
||||
key={String(hasWhereParam.current && !params?.where)}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
{enableSort && (
|
||||
|
||||
@@ -3,13 +3,14 @@ import type { ClientCollectionConfig, Where } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
|
||||
import type { ListDrawerProps } from './types.js'
|
||||
|
||||
import { SelectMany } from '../../elements/SelectMany/index.js'
|
||||
import { FieldLabel } from '../../fields/FieldLabel/index.js'
|
||||
import { usePayloadAPI } from '../../hooks/usePayloadAPI.js'
|
||||
import { useThrottledEffect } from '../../hooks/useThrottledEffect.js'
|
||||
import { XIcon } from '../../icons/X/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
@@ -54,13 +55,25 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { permissions } = useAuth()
|
||||
const { setPreference } = usePreferences()
|
||||
const { getPreference, setPreference } = usePreferences()
|
||||
const { closeModal, isModalOpen } = useModal()
|
||||
const [limit, setLimit] = useState<number>()
|
||||
// Track the page limit so we can reset the page number when it changes
|
||||
const previousLimit = useRef<number>(limit || null)
|
||||
const [sort, setSort] = useState<string>(null)
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [where, setWhere] = useState<Where>(null)
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const [showLoadingOverlay, setShowLoadingOverlay] = useState<boolean>(true)
|
||||
const hasInitialised = useRef(false)
|
||||
|
||||
const params = {
|
||||
limit,
|
||||
page,
|
||||
search,
|
||||
sort,
|
||||
where,
|
||||
}
|
||||
|
||||
const {
|
||||
config: {
|
||||
@@ -94,12 +107,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
: undefined,
|
||||
)
|
||||
|
||||
// const [fields, setFields] = useState<Field[]>(() => formatFields(selectedCollectionConfig))
|
||||
|
||||
useEffect(() => {
|
||||
// setFields(formatFields(selectedCollectionConfig))
|
||||
}, [selectedCollectionConfig])
|
||||
|
||||
// allow external control of selected collection, same as the initial state logic above
|
||||
useEffect(() => {
|
||||
if (selectedCollection) {
|
||||
@@ -111,7 +118,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}
|
||||
}, [selectedCollection, enabledCollectionConfigs, onSelect, t])
|
||||
|
||||
const preferenceKey = `${selectedCollectionConfig.slug}-list`
|
||||
const preferencesKey = `${selectedCollectionConfig.slug}-list`
|
||||
|
||||
// this is the 'create new' drawer
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { drawerSlug: documentDrawerSlug }] =
|
||||
@@ -147,6 +154,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
admin: { listSearchableFields, useAsTitle } = {},
|
||||
versions,
|
||||
} = selectedCollectionConfig
|
||||
|
||||
const params: {
|
||||
cacheBust?: number
|
||||
depth?: number
|
||||
@@ -194,6 +202,17 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
if (cacheBust) {
|
||||
params.cacheBust = cacheBust
|
||||
}
|
||||
if (limit) {
|
||||
params.limit = limit
|
||||
|
||||
if (limit !== previousLimit.current) {
|
||||
previousLimit.current = limit
|
||||
|
||||
// Reset page if limit changes
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setPage(1)
|
||||
}
|
||||
}
|
||||
if (copyOfWhere) {
|
||||
params.where = copyOfWhere
|
||||
}
|
||||
@@ -202,7 +221,18 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}
|
||||
|
||||
setParams(params)
|
||||
}, [page, sort, where, search, cacheBust, filterOptions, selectedCollectionConfig, t, setParams])
|
||||
}, [
|
||||
page,
|
||||
sort,
|
||||
where,
|
||||
search,
|
||||
limit,
|
||||
cacheBust,
|
||||
filterOptions,
|
||||
selectedCollectionConfig,
|
||||
t,
|
||||
setParams,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const newPreferences = {
|
||||
@@ -210,8 +240,49 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
sort,
|
||||
}
|
||||
|
||||
void setPreference(preferenceKey, newPreferences, true)
|
||||
}, [sort, limit, setPreference, preferenceKey])
|
||||
if (limit || sort) {
|
||||
void setPreference(preferencesKey, newPreferences, true)
|
||||
}
|
||||
}, [sort, limit, setPreference, preferencesKey])
|
||||
|
||||
// Get existing preferences if they exist
|
||||
useEffect(() => {
|
||||
if (preferencesKey && !limit) {
|
||||
const getInitialPref = async () => {
|
||||
const existingPreferences = await getPreference<{ limit?: number }>(preferencesKey)
|
||||
|
||||
if (existingPreferences?.limit) {
|
||||
setLimit(existingPreferences?.limit)
|
||||
}
|
||||
}
|
||||
void getInitialPref()
|
||||
}
|
||||
}, [getPreference, limit, preferencesKey])
|
||||
|
||||
useThrottledEffect(
|
||||
() => {
|
||||
if (isLoadingList) {
|
||||
setShowLoadingOverlay(true)
|
||||
}
|
||||
},
|
||||
1750,
|
||||
[isLoadingList, setShowLoadingOverlay],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
hasInitialised.current = true
|
||||
} else {
|
||||
hasInitialised.current = false
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingList && showLoadingOverlay) {
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setShowLoadingOverlay(false)
|
||||
}
|
||||
}, [isLoadingList, showLoadingOverlay])
|
||||
|
||||
const onCreateNew = useCallback(
|
||||
({ doc }) => {
|
||||
@@ -232,14 +303,14 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
return null
|
||||
}
|
||||
|
||||
if (isLoadingList) {
|
||||
return <LoadingOverlay />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLoadingOverlay && <LoadingOverlay />}
|
||||
<ListInfoProvider
|
||||
beforeActions={
|
||||
enableRowSelections ? [<SelectMany key="select-many" onClick={onBulkSelect} />] : undefined
|
||||
enableRowSelections
|
||||
? [<SelectMany key="select-many" onClick={onBulkSelect} />]
|
||||
: undefined
|
||||
}
|
||||
collectionConfig={selectedCollectionConfig}
|
||||
collectionSlug={selectedCollectionConfig.slug}
|
||||
@@ -309,7 +380,9 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
handleSortChange={setSort}
|
||||
handleWhereChange={setWhere}
|
||||
modifySearchParams={false}
|
||||
preferenceKey={preferenceKey}
|
||||
// @ts-expect-error todo: fix types
|
||||
params={params}
|
||||
preferenceKey={preferencesKey}
|
||||
>
|
||||
<TableColumnsProvider
|
||||
cellProps={[
|
||||
@@ -328,12 +401,13 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
]}
|
||||
collectionSlug={selectedCollectionConfig.slug}
|
||||
enableRowSelections={enableRowSelections}
|
||||
preferenceKey={preferenceKey}
|
||||
preferenceKey={preferencesKey}
|
||||
>
|
||||
<RenderComponent mappedComponent={List} />
|
||||
<DocumentDrawer onSave={onCreateNew} />
|
||||
</TableColumnsProvider>
|
||||
</ListQueryProvider>
|
||||
</ListInfoProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export type SearchFilterProps = {
|
||||
fieldName?: string
|
||||
@@ -12,32 +12,53 @@ export type SearchFilterProps = {
|
||||
|
||||
import type { ParsedQs } from 'qs-esm'
|
||||
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
|
||||
import { useDebounce } from '../../hooks/useDebounce.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'search-filter'
|
||||
|
||||
export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
|
||||
const { handleChange, initialParams, label, setValue, value } = props
|
||||
|
||||
const previousSearch = useRef(
|
||||
typeof initialParams?.search === 'string' ? initialParams?.search : '',
|
||||
const { handleChange, initialParams, label } = props
|
||||
const pathname = usePathname()
|
||||
const [search, setSearch] = useState(
|
||||
typeof initialParams?.search === 'string' ? initialParams?.search : undefined,
|
||||
)
|
||||
|
||||
const debouncedSearch = useDebounce(value, 300)
|
||||
/**
|
||||
* Tracks whether the state should be updated based on the search value.
|
||||
* If the value is updated from the URL, we don't want to update the state as it causes additional renders.
|
||||
*/
|
||||
const shouldUpdateState = useRef(true)
|
||||
|
||||
/**
|
||||
* Tracks the previous search value to compare with the current debounced search value.
|
||||
*/
|
||||
const previousSearch = useRef(
|
||||
typeof initialParams?.search === 'string' ? initialParams?.search : undefined,
|
||||
)
|
||||
|
||||
const debouncedSearch = useDebounce(search, 300)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch !== previousSearch.current) {
|
||||
if (initialParams?.search !== previousSearch.current) {
|
||||
shouldUpdateState.current = false
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setSearch(initialParams?.search as string)
|
||||
previousSearch.current = initialParams?.search as string
|
||||
}
|
||||
}, [initialParams?.search, pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch !== previousSearch.current && shouldUpdateState.current) {
|
||||
if (handleChange) {
|
||||
handleChange(debouncedSearch)
|
||||
}
|
||||
|
||||
previousSearch.current = debouncedSearch
|
||||
}
|
||||
}, [debouncedSearch, previousSearch, handleChange])
|
||||
|
||||
// Cleans up the search input when the component is unmounted
|
||||
useEffect(() => () => setValue(''), [])
|
||||
}, [debouncedSearch, handleChange])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -45,10 +66,13 @@ export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
|
||||
aria-label={label}
|
||||
className={`${baseClass}__input`}
|
||||
id="search-filter-input"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onChange={(e) => {
|
||||
shouldUpdateState.current = true
|
||||
setSearch(e.target.value)
|
||||
}}
|
||||
placeholder={label}
|
||||
type="text"
|
||||
value={value || ''}
|
||||
value={search || ''}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react'
|
||||
|
||||
import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -20,11 +19,10 @@ const baseClass = 'sort-column'
|
||||
|
||||
export const SortColumn: React.FC<SortColumnProps> = (props) => {
|
||||
const { name, disable = false, Label, label } = props
|
||||
const { searchParams } = useSearchParams()
|
||||
const { handleSortChange } = useListQuery()
|
||||
const { handleSortChange, params } = useListQuery()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { sort } = searchParams
|
||||
const { sort } = params
|
||||
|
||||
const desc = `-${name}`
|
||||
const asc = name
|
||||
|
||||
@@ -7,7 +7,6 @@ import React, { useEffect, useState } from 'react'
|
||||
import type { WhereBuilderProps } from './types.js'
|
||||
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { Condition } from './Condition/index.js'
|
||||
@@ -31,11 +30,11 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
const [reducedFields, setReducedColumns] = useState(() => reduceClientFields({ fields, i18n }))
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setReducedColumns(reduceClientFields({ fields, i18n }))
|
||||
}, [fields, i18n])
|
||||
|
||||
const { searchParams } = useSearchParams()
|
||||
const { handleWhereChange } = useListQuery()
|
||||
const { handleWhereChange, params } = useListQuery()
|
||||
const [shouldUpdateQuery, setShouldUpdateQuery] = React.useState(false)
|
||||
|
||||
// This handles initializing the where conditions from the search query (URL). That way, if you pass in
|
||||
@@ -67,7 +66,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
*/
|
||||
|
||||
const [conditions, setConditions] = React.useState(() => {
|
||||
const whereFromSearch = searchParams.where
|
||||
const whereFromSearch = params.where
|
||||
if (whereFromSearch) {
|
||||
if (validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or
|
||||
|
||||
@@ -9,6 +9,13 @@ type useThrottledEffect = (
|
||||
deps: React.DependencyList,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A hook that will throttle the execution of a callback function inside a useEffect.
|
||||
* This is useful for things like throttling loading states or other UI updates.
|
||||
* @param callback The callback function to be executed.
|
||||
* @param delay The delay in milliseconds to throttle the callback.
|
||||
* @param deps The dependencies to watch for changes.
|
||||
*/
|
||||
export const useThrottledEffect: useThrottledEffect = (callback, delay, deps = []) => {
|
||||
const lastRan = useRef(Date.now())
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PaginatedDocs, Where } from 'payload'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import * as qs from 'qs-esm'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { Column } from '../../elements/Table/index.js'
|
||||
|
||||
@@ -27,6 +27,7 @@ type ContextHandlers = {
|
||||
handleSearchChange?: (search: string) => Promise<void>
|
||||
handleSortChange?: (sort: string) => Promise<void>
|
||||
handleWhereChange?: (where: Where) => Promise<void>
|
||||
params: RefineOverrides
|
||||
}
|
||||
|
||||
export type ListQueryProps = {
|
||||
@@ -35,6 +36,11 @@ export type ListQueryProps = {
|
||||
readonly defaultLimit?: number
|
||||
readonly defaultSort?: string
|
||||
readonly modifySearchParams?: boolean
|
||||
/**
|
||||
* Used to manage the query params manually. If you pass this prop, the provider will not manage the query params from the searchParams.
|
||||
* Useful for modals or other components that need to manage the query params themselves.
|
||||
*/
|
||||
readonly params?: RefineOverrides
|
||||
readonly preferenceKey?: string
|
||||
} & PropHandlers
|
||||
|
||||
@@ -68,14 +74,15 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
handleSortChange: handleSortChangeFromProps,
|
||||
handleWhereChange: handleWhereChangeFromProps,
|
||||
modifySearchParams,
|
||||
params: paramsFromProps,
|
||||
preferenceKey,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { setPreference } = usePreferences()
|
||||
const hasSetInitialParams = React.useRef(false)
|
||||
const { searchParams: currentQuery } = useSearchParams()
|
||||
const [params, setParams] = useState(paramsFromProps || currentQuery)
|
||||
|
||||
const refineListData = React.useCallback(
|
||||
const refineListData = useCallback(
|
||||
async (query: RefineOverrides) => {
|
||||
if (!modifySearchParams) {
|
||||
return
|
||||
@@ -114,10 +121,20 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
|
||||
router.replace(`${qs.stringify(params, { addQueryPrefix: true })}`)
|
||||
},
|
||||
[preferenceKey, modifySearchParams, router, setPreference, currentQuery],
|
||||
[
|
||||
modifySearchParams,
|
||||
currentQuery?.page,
|
||||
currentQuery?.limit,
|
||||
currentQuery?.search,
|
||||
currentQuery?.sort,
|
||||
currentQuery?.where,
|
||||
preferenceKey,
|
||||
router,
|
||||
setPreference,
|
||||
],
|
||||
)
|
||||
|
||||
const handlePageChange = React.useCallback(
|
||||
const handlePageChange = useCallback(
|
||||
async (arg: number) => {
|
||||
if (typeof handlePageChangeFromProps === 'function') {
|
||||
await handlePageChangeFromProps(arg)
|
||||
@@ -134,23 +151,25 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
await handlePerPageChangeFromProps(arg)
|
||||
}
|
||||
|
||||
await refineListData({ limit: String(arg) })
|
||||
await refineListData({ limit: String(arg), page: '1' })
|
||||
},
|
||||
[refineListData, handlePerPageChangeFromProps],
|
||||
)
|
||||
|
||||
const handleSearchChange = React.useCallback(
|
||||
const handleSearchChange = useCallback(
|
||||
async (arg: string) => {
|
||||
const search = arg === '' ? undefined : arg
|
||||
|
||||
if (typeof handleSearchChangeFromProps === 'function') {
|
||||
await handleSearchChangeFromProps(arg)
|
||||
await handleSearchChangeFromProps(search)
|
||||
}
|
||||
|
||||
await refineListData({ search: arg })
|
||||
await refineListData({ search })
|
||||
},
|
||||
[refineListData, handleSearchChangeFromProps],
|
||||
[handleSearchChangeFromProps, refineListData],
|
||||
)
|
||||
|
||||
const handleSortChange = React.useCallback(
|
||||
const handleSortChange = useCallback(
|
||||
async (arg: string) => {
|
||||
if (typeof handleSortChangeFromProps === 'function') {
|
||||
await handleSortChangeFromProps(arg)
|
||||
@@ -161,7 +180,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
[refineListData, handleSortChangeFromProps],
|
||||
)
|
||||
|
||||
const handleWhereChange = React.useCallback(
|
||||
const handleWhereChange = useCallback(
|
||||
async (arg: Where) => {
|
||||
if (typeof handleWhereChangeFromProps === 'function') {
|
||||
await handleWhereChangeFromProps(arg)
|
||||
@@ -172,8 +191,11 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
[refineListData, handleWhereChangeFromProps],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasSetInitialParams.current) {
|
||||
useEffect(() => {
|
||||
if (paramsFromProps) {
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setParams(paramsFromProps)
|
||||
} else {
|
||||
if (modifySearchParams) {
|
||||
let shouldUpdateQueryString = false
|
||||
|
||||
@@ -187,14 +209,15 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
shouldUpdateQueryString = true
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setParams(currentQuery)
|
||||
|
||||
if (shouldUpdateQueryString) {
|
||||
router.replace(`?${qs.stringify(currentQuery)}`)
|
||||
}
|
||||
}
|
||||
|
||||
hasSetInitialParams.current = true
|
||||
}
|
||||
}, [defaultSort, defaultLimit, router, modifySearchParams, currentQuery])
|
||||
}, [defaultSort, defaultLimit, router, modifySearchParams, currentQuery, paramsFromProps, params])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
@@ -205,6 +228,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
handleSearchChange,
|
||||
handleSortChange,
|
||||
handleWhereChange,
|
||||
params,
|
||||
refineListData,
|
||||
}}
|
||||
>
|
||||
|
||||
31
test/fields/components/AfterNavLinks.tsx
Normal file
31
test/fields/components/AfterNavLinks.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadClientReactComponent, SanitizedConfig } from 'payload'
|
||||
|
||||
import { NavGroup, useConfig } from '@payloadcms/ui'
|
||||
import LinkImport from 'next/link.js'
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
import React from 'react'
|
||||
|
||||
const baseClass = 'after-nav-links'
|
||||
|
||||
export const AfterNavLinks: PayloadClientReactComponent<
|
||||
SanitizedConfig['admin']['components']['afterNavLinks'][0]
|
||||
> = () => {
|
||||
const {
|
||||
config: {
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
return (
|
||||
<NavGroup key="extra-links" label="Extra Links">
|
||||
{/* Open link to payload admin url */}
|
||||
{/* <Link href={`${adminRoute}/collections/uploads`}>Internal Payload Admin Link</Link> */}
|
||||
{/* Open link to payload admin url with prefiltered query */}
|
||||
<Link href={`${adminRoute}/collections/uploads?page=1&search=jpg&limit=10`}>
|
||||
Prefiltered Media
|
||||
</Link>
|
||||
</NavGroup>
|
||||
)
|
||||
}
|
||||
@@ -105,6 +105,9 @@ export default buildConfigWithDefaults({
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
components: {
|
||||
afterNavLinks: ['/components/AfterNavLinks.js#AfterNavLinks'],
|
||||
},
|
||||
custom: {
|
||||
client: {
|
||||
'new-value': 'client available',
|
||||
|
||||
Reference in New Issue
Block a user