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 { 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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 || {}) }
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user