Skip to content

Commit

Permalink
fix(ui): bulk update and delete ignoring search query (#9377)
Browse files Browse the repository at this point in the history
Fixes #9374.
  • Loading branch information
jacobsfletch authored Nov 20, 2024
1 parent 439dcd4 commit ef37483
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 45 deletions.
19 changes: 15 additions & 4 deletions packages/ui/src/elements/DeleteMany/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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 {
Expand All @@ -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]
Expand All @@ -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',
Expand Down Expand Up @@ -107,6 +116,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
}
})
}, [
searchParams,
addDefaultError,
api,
getQueryParams,
Expand All @@ -123,6 +133,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
toggleAll,
toggleModal,
clearRouteCache,
collection,
])

if (selectAll === SelectAllStatus.None || !hasDeletePermission) {
Expand Down
28 changes: 19 additions & 9 deletions packages/ui/src/elements/EditMany/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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}
/>
)}
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/providers/Selection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
}
})
}

setSelected(rows)
},
[docs, selectAll, user?.id],
Expand Down Expand Up @@ -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: '' },
}
Expand All @@ -127,11 +130,13 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
},
}
}

if (additionalWhereParams) {
where = {
and: [{ ...additionalWhereParams }, where],
}
}

return qs.stringify(
{
locale,
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/utilities/mergeListSearchAndWhere.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {}) }

Expand Down
4 changes: 2 additions & 2 deletions test/_community/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export interface Config {
user: User & {
collection: 'users';
};
jobs: {
jobs?: {
tasks: unknown;
workflows: unknown;
workflows?: unknown;
};
}
export interface UserAuthOperations {
Expand Down
93 changes: 66 additions & 27 deletions test/admin/e2e/3/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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(updatedPostTitle)
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()

await titleInput.fill(bulkTitle)
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
'Updated 3 Posts successfully.',
)

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 3 Posts successfully.',
'Updated 1 Post 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('.table table > tbody > tr')).toHaveCount(1)
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
})

test('should save globals', async () => {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
],
"paths": {
"@payload-config": [
"./test/versions/config.ts"
"./test/_community/config.ts"
],
"@payloadcms/live-preview": [
"./packages/live-preview/src"
Expand Down

0 comments on commit ef37483

Please sign in to comment.