fix(ui): bulk update and delete ignoring search query (#9377)

Fixes #9374.
This commit is contained in:
Jacob Fletcher
2024-11-20 13:22:43 -05:00
committed by GitHub
parent 439dcd493e
commit ef3748319e
7 changed files with 111 additions and 46 deletions

View File

@@ -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) {

View File

@@ -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}
/>
)}

View File

@@ -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,

View File

@@ -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 || {}) }

View File

@@ -40,9 +40,9 @@ export interface Config {
user: User & {
collection: 'users';
};
jobs: {
jobs?: {
tasks: unknown;
workflows: unknown;
workflows?: unknown;
};
}
export interface UserAuthOperations {

View File

@@ -452,38 +452,46 @@ describe('admin3', () => {
expect(page.url()).toContain(postsUrl.list)
})
test('should bulk delete', async () => {
async function selectAndDeleteAll() {
test('should bulk delete all on page', async () => {
await deleteAllPosts()
await Promise.all([createPost(), createPost(), createPost()])
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
await deleteAllPosts()
await createPost()
await createPost()
await createPost()
await page.goto(postsUrl.list)
await selectAndDeleteAll()
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 () => {

View File

@@ -37,7 +37,7 @@
],
"paths": {
"@payload-config": [
"./test/versions/config.ts"
"./test/_community/config.ts"
],
"@payloadcms/live-preview": [
"./packages/live-preview/src"