fix(ui): bulk update and delete ignoring search query (#9377)
Fixes #9374.
This commit is contained in:
@@ -14,9 +14,10 @@ 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 { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { Pill } from '../Pill/index.js'
|
||||
import './index.scss'
|
||||
import { Pill } from '../Pill/index.js'
|
||||
|
||||
const baseClass = 'delete-documents'
|
||||
|
||||
@@ -26,7 +27,7 @@ export type Props = {
|
||||
}
|
||||
|
||||
export const DeleteMany: React.FC<Props> = (props) => {
|
||||
const { collection: { slug, labels: { plural, singular } } = {} } = props
|
||||
const { collection, collection: { slug, labels: { plural, singular } } = {} } = props
|
||||
|
||||
const { permissions } = useAuth()
|
||||
const {
|
||||
@@ -40,7 +41,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const router = useRouter()
|
||||
const { stringifyParams } = useSearchParams()
|
||||
const { searchParams, stringifyParams } = useSearchParams()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
@@ -54,8 +55,16 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
setDeleting(true)
|
||||
|
||||
const queryWithSearch = mergeListSearchAndWhere({
|
||||
collectionConfig: collection,
|
||||
search: searchParams?.search as string,
|
||||
})
|
||||
|
||||
const queryString = getQueryParams(queryWithSearch)
|
||||
|
||||
await requests
|
||||
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
|
||||
.delete(`${serverURL}${api}/${slug}${queryString}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -107,6 +116,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
}, [
|
||||
searchParams,
|
||||
addDefaultError,
|
||||
api,
|
||||
getQueryParams,
|
||||
@@ -123,6 +133,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
toggleAll,
|
||||
toggleModal,
|
||||
clearRouteCache,
|
||||
collection,
|
||||
])
|
||||
|
||||
if (selectAll === SelectAllStatus.None || !hasDeletePermission) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ClientCollectionConfig, FormState } from 'payload'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import type { FormProps } from '../../forms/Form/index.js'
|
||||
|
||||
@@ -24,9 +24,10 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j
|
||||
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 { FieldSelect } from '../FieldSelect/index.js'
|
||||
import './index.scss'
|
||||
import { FieldSelect } from '../FieldSelect/index.js'
|
||||
|
||||
const baseClass = 'edit-many'
|
||||
|
||||
@@ -124,7 +125,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
const { count, getQueryParams, selectAll } = useSelection()
|
||||
const { i18n, t } = useTranslation()
|
||||
const [selected, setSelected] = useState([])
|
||||
const { stringifyParams } = useSearchParams()
|
||||
const { searchParams, stringifyParams } = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [initialState, setInitialState] = useState<FormState>()
|
||||
const hasInitializedState = React.useRef(false)
|
||||
@@ -191,9 +192,14 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
|
||||
return null
|
||||
}
|
||||
const queryString = useMemo(() => {
|
||||
const queryWithSearch = mergeListSearchAndWhere({
|
||||
collectionConfig: collection,
|
||||
search: searchParams?.search as string,
|
||||
})
|
||||
|
||||
return getQueryParams(queryWithSearch)
|
||||
}, [collection, searchParams, getQueryParams])
|
||||
|
||||
const onSuccess = () => {
|
||||
router.replace(
|
||||
@@ -205,6 +211,10 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
closeModal(drawerSlug)
|
||||
}
|
||||
|
||||
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<DrawerToggler
|
||||
@@ -272,17 +282,17 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
|
||||
{collection?.versions?.drafts ? (
|
||||
<React.Fragment>
|
||||
<SaveDraftButton
|
||||
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}&draft=true`}
|
||||
action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
|
||||
disabled={selected.length === 0}
|
||||
/>
|
||||
<PublishButton
|
||||
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}&draft=true`}
|
||||
action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
|
||||
disabled={selected.length === 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Submit
|
||||
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}`}
|
||||
action={`${serverURL}${apiRoute}/${slug}${queryString}`}
|
||||
disabled={selected.length === 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -75,6 +75,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setSelected(rows)
|
||||
},
|
||||
[docs, selectAll, user?.id],
|
||||
@@ -107,8 +108,10 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
|
||||
const getQueryParams = useCallback(
|
||||
(additionalWhereParams?: Where): string => {
|
||||
let where: Where
|
||||
|
||||
if (selectAll === SelectAllStatus.AllAvailable) {
|
||||
const params = searchParams?.where as Where
|
||||
|
||||
where = params || {
|
||||
id: { not_equals: '' },
|
||||
}
|
||||
@@ -127,11 +130,13 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalWhereParams) {
|
||||
where = {
|
||||
and: [{ ...additionalWhereParams }, where],
|
||||
}
|
||||
}
|
||||
|
||||
return qs.stringify(
|
||||
{
|
||||
locale,
|
||||
|
||||
@@ -29,10 +29,10 @@ export const hoistQueryParamsToAnd = (currentWhere: Where, incomingWhere: Where)
|
||||
type Args = {
|
||||
collectionConfig: ClientCollectionConfig | SanitizedCollectionConfig
|
||||
search: string
|
||||
where: Where
|
||||
where?: Where
|
||||
}
|
||||
|
||||
export const mergeListSearchAndWhere = ({ collectionConfig, search, where }: Args): Where => {
|
||||
export const mergeListSearchAndWhere = ({ collectionConfig, search, where = {} }: Args): Where => {
|
||||
if (search) {
|
||||
let copyOfWhere = { ...(where || {}) }
|
||||
|
||||
|
||||
@@ -40,9 +40,9 @@ export interface Config {
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
jobs?: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
workflows?: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
|
||||
@@ -452,38 +452,46 @@ describe('admin3', () => {
|
||||
expect(page.url()).toContain(postsUrl.list)
|
||||
})
|
||||
|
||||
test('should bulk delete', async () => {
|
||||
async function selectAndDeleteAll() {
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#confirm-delete').click()
|
||||
}
|
||||
|
||||
// First, delete all posts created by the seed
|
||||
test('should bulk delete all on page', async () => {
|
||||
await deleteAllPosts()
|
||||
await createPost()
|
||||
await createPost()
|
||||
await createPost()
|
||||
|
||||
await Promise.all([createPost(), createPost(), createPost()])
|
||||
await page.goto(postsUrl.list)
|
||||
await selectAndDeleteAll()
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#confirm-delete').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 3 Posts successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.collection-list__no-results')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should bulk delete with filters and across pages', async () => {
|
||||
await deleteAllPosts()
|
||||
await Promise.all([createPost({ title: 'Post 1' }), createPost({ title: 'Post 2' })])
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post 1')
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button.list-selection__button').click()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#confirm-delete').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Deleted 1 Post successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should bulk update', async () => {
|
||||
// First, delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
await createPost()
|
||||
await createPost()
|
||||
await createPost()
|
||||
|
||||
const bulkTitle = 'Bulk update title'
|
||||
const post1Title = 'Post'
|
||||
const updatedPostTitle = `${post1Title} (Updated)`
|
||||
await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
@@ -493,21 +501,52 @@ describe('admin3', () => {
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
|
||||
await expect(titleInput).toBeVisible()
|
||||
|
||||
await titleInput.fill(bulkTitle)
|
||||
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 3 Posts successfully.',
|
||||
)
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(bulkTitle)
|
||||
await expect(page.locator('.row-2 .cell-title')).toContainText(bulkTitle)
|
||||
await expect(page.locator('.row-3 .cell-title')).toContainText(bulkTitle)
|
||||
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
||||
await expect(page.locator('.row-2 .cell-title')).toContainText(updatedPostTitle)
|
||||
await expect(page.locator('.row-3 .cell-title')).toContainText(updatedPostTitle)
|
||||
})
|
||||
|
||||
test('should bulk update with filters and across pages', async () => {
|
||||
// First, delete all posts created by the seed
|
||||
await deleteAllPosts()
|
||||
const post1Title = 'Post 1'
|
||||
await Promise.all([createPost({ title: post1Title }), createPost({ title: 'Post 2' })])
|
||||
const updatedPostTitle = `${post1Title} (Updated)`
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('#search-filter-input').fill('Post 1')
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('button.list-selection__button').click()
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const titleOption = page.locator('.field-select .rs__option', {
|
||||
hasText: exactText('Title'),
|
||||
})
|
||||
|
||||
await expect(titleOption).toBeVisible()
|
||||
await titleOption.click()
|
||||
const titleInput = page.locator('#field-title')
|
||||
await expect(titleInput).toBeVisible()
|
||||
await titleInput.fill(updatedPostTitle)
|
||||
|
||||
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Updated 1 Post successfully.',
|
||||
)
|
||||
|
||||
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
|
||||
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
|
||||
})
|
||||
|
||||
test('should save globals', async () => {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/versions/config.ts"
|
||||
"./test/_community/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user