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 { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
import { Button } from '../Button/index.js' import { Button } from '../Button/index.js'
import { Pill } from '../Pill/index.js'
import './index.scss' import './index.scss'
import { Pill } from '../Pill/index.js'
const baseClass = 'delete-documents' const baseClass = 'delete-documents'
@@ -26,7 +27,7 @@ export type Props = {
} }
export const DeleteMany: React.FC<Props> = (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 { permissions } = useAuth()
const { const {
@@ -40,7 +41,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const router = useRouter() const router = useRouter()
const { stringifyParams } = useSearchParams() const { searchParams, stringifyParams } = useSearchParams()
const { clearRouteCache } = useRouteCache() const { clearRouteCache } = useRouteCache()
const collectionPermissions = permissions?.collections?.[slug] const collectionPermissions = permissions?.collections?.[slug]
@@ -54,8 +55,16 @@ export const DeleteMany: React.FC<Props> = (props) => {
const handleDelete = useCallback(async () => { const handleDelete = useCallback(async () => {
setDeleting(true) setDeleting(true)
const queryWithSearch = mergeListSearchAndWhere({
collectionConfig: collection,
search: searchParams?.search as string,
})
const queryString = getQueryParams(queryWithSearch)
await requests await requests
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, { .delete(`${serverURL}${api}/${slug}${queryString}`, {
headers: { headers: {
'Accept-Language': i18n.language, 'Accept-Language': i18n.language,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -107,6 +116,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
} }
}) })
}, [ }, [
searchParams,
addDefaultError, addDefaultError,
api, api,
getQueryParams, getQueryParams,
@@ -123,6 +133,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
toggleAll, toggleAll,
toggleModal, toggleModal,
clearRouteCache, clearRouteCache,
collection,
]) ])
if (selectAll === SelectAllStatus.None || !hasDeletePermission) { if (selectAll === SelectAllStatus.None || !hasDeletePermission) {

View File

@@ -4,7 +4,7 @@ import type { ClientCollectionConfig, FormState } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' 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' 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 { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js' import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
import { Drawer, DrawerToggler } from '../Drawer/index.js' import { Drawer, DrawerToggler } from '../Drawer/index.js'
import { FieldSelect } from '../FieldSelect/index.js'
import './index.scss' import './index.scss'
import { FieldSelect } from '../FieldSelect/index.js'
const baseClass = 'edit-many' const baseClass = 'edit-many'
@@ -124,7 +125,7 @@ export const EditMany: React.FC<EditManyProps> = (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 { stringifyParams } = useSearchParams() const { searchParams, stringifyParams } = useSearchParams()
const router = useRouter() const router = useRouter()
const [initialState, setInitialState] = useState<FormState>() const [initialState, setInitialState] = useState<FormState>()
const hasInitializedState = React.useRef(false) const hasInitializedState = React.useRef(false)
@@ -191,9 +192,14 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
} }
}, []) }, [])
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) { const queryString = useMemo(() => {
return null const queryWithSearch = mergeListSearchAndWhere({
} collectionConfig: collection,
search: searchParams?.search as string,
})
return getQueryParams(queryWithSearch)
}, [collection, searchParams, getQueryParams])
const onSuccess = () => { const onSuccess = () => {
router.replace( router.replace(
@@ -205,6 +211,10 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
closeModal(drawerSlug) closeModal(drawerSlug)
} }
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
return null
}
return ( return (
<div className={baseClass}> <div className={baseClass}>
<DrawerToggler <DrawerToggler
@@ -272,17 +282,17 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
{collection?.versions?.drafts ? ( {collection?.versions?.drafts ? (
<React.Fragment> <React.Fragment>
<SaveDraftButton <SaveDraftButton
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}&draft=true`} action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
disabled={selected.length === 0} disabled={selected.length === 0}
/> />
<PublishButton <PublishButton
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}&draft=true`} action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
disabled={selected.length === 0} disabled={selected.length === 0}
/> />
</React.Fragment> </React.Fragment>
) : ( ) : (
<Submit <Submit
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}`} action={`${serverURL}${apiRoute}/${slug}${queryString}`}
disabled={selected.length === 0} disabled={selected.length === 0}
/> />
)} )}

View File

@@ -75,6 +75,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
} }
}) })
} }
setSelected(rows) setSelected(rows)
}, },
[docs, selectAll, user?.id], [docs, selectAll, user?.id],
@@ -107,8 +108,10 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
const getQueryParams = useCallback( const getQueryParams = useCallback(
(additionalWhereParams?: Where): string => { (additionalWhereParams?: Where): string => {
let where: Where let where: Where
if (selectAll === SelectAllStatus.AllAvailable) { if (selectAll === SelectAllStatus.AllAvailable) {
const params = searchParams?.where as Where const params = searchParams?.where as Where
where = params || { where = params || {
id: { not_equals: '' }, id: { not_equals: '' },
} }
@@ -127,11 +130,13 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
}, },
} }
} }
if (additionalWhereParams) { if (additionalWhereParams) {
where = { where = {
and: [{ ...additionalWhereParams }, where], and: [{ ...additionalWhereParams }, where],
} }
} }
return qs.stringify( return qs.stringify(
{ {
locale, locale,

View File

@@ -29,10 +29,10 @@ export const hoistQueryParamsToAnd = (currentWhere: Where, incomingWhere: Where)
type Args = { type Args = {
collectionConfig: ClientCollectionConfig | SanitizedCollectionConfig collectionConfig: ClientCollectionConfig | SanitizedCollectionConfig
search: string search: string
where: Where where?: Where
} }
export const mergeListSearchAndWhere = ({ collectionConfig, search, where }: Args): Where => { export const mergeListSearchAndWhere = ({ collectionConfig, search, where = {} }: Args): Where => {
if (search) { if (search) {
let copyOfWhere = { ...(where || {}) } let copyOfWhere = { ...(where || {}) }

View File

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

View File

@@ -452,38 +452,46 @@ describe('admin3', () => {
expect(page.url()).toContain(postsUrl.list) expect(page.url()).toContain(postsUrl.list)
}) })
test('should bulk delete', async () => { test('should bulk delete all on page', async () => {
async function selectAndDeleteAll() { await deleteAllPosts()
await Promise.all([createPost(), createPost(), createPost()])
await page.goto(postsUrl.list) await page.goto(postsUrl.list)
await page.locator('input#select-all').check() await page.locator('input#select-all').check()
await page.locator('.delete-documents__toggle').click() await page.locator('.delete-documents__toggle').click()
await page.locator('#confirm-delete').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( await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
'Deleted 3 Posts successfully.', 'Deleted 3 Posts successfully.',
) )
await expect(page.locator('.collection-list__no-results')).toBeVisible() 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 () => { test('should bulk update', async () => {
// First, delete all posts created by the seed // First, delete all posts created by the seed
await deleteAllPosts() await deleteAllPosts()
await createPost() const post1Title = 'Post'
await createPost() const updatedPostTitle = `${post1Title} (Updated)`
await createPost() await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
const bulkTitle = 'Bulk update title'
await page.goto(postsUrl.list) await page.goto(postsUrl.list)
await page.locator('input#select-all').check() await page.locator('input#select-all').check()
await page.locator('.edit-many__toggle').click() await page.locator('.edit-many__toggle').click()
await page.locator('.field-select .rs__control').click() await page.locator('.field-select .rs__control').click()
@@ -493,21 +501,52 @@ describe('admin3', () => {
}) })
await expect(titleOption).toBeVisible() await expect(titleOption).toBeVisible()
await titleOption.click() await titleOption.click()
const titleInput = page.locator('#field-title') const titleInput = page.locator('#field-title')
await expect(titleInput).toBeVisible() await expect(titleInput).toBeVisible()
await titleInput.fill(updatedPostTitle)
await titleInput.fill(bulkTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click() await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toContainText( await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 3 Posts successfully.', '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-1 .cell-title')).toContainText(updatedPostTitle)
await expect(page.locator('.row-3 .cell-title')).toContainText(bulkTitle) 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 () => { test('should save globals', async () => {

View File

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