Skip to content

Commit ef37483

Browse files
authored
fix(ui): bulk update and delete ignoring search query (#9377)
Fixes #9374.
1 parent 439dcd4 commit ef37483

File tree

7 files changed

+110
-45
lines changed

7 files changed

+110
-45
lines changed

packages/ui/src/elements/DeleteMany/index.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import { useSearchParams } from '../../providers/SearchParams/index.js'
1414
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
1515
import { useTranslation } from '../../providers/Translation/index.js'
1616
import { requests } from '../../utilities/api.js'
17+
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
1718
import { Button } from '../Button/index.js'
18-
import { Pill } from '../Pill/index.js'
1919
import './index.scss'
20+
import { Pill } from '../Pill/index.js'
2021

2122
const baseClass = 'delete-documents'
2223

@@ -26,7 +27,7 @@ export type Props = {
2627
}
2728

2829
export const DeleteMany: React.FC<Props> = (props) => {
29-
const { collection: { slug, labels: { plural, singular } } = {} } = props
30+
const { collection, collection: { slug, labels: { plural, singular } } = {} } = props
3031

3132
const { permissions } = useAuth()
3233
const {
@@ -40,7 +41,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
4041
const { i18n, t } = useTranslation()
4142
const [deleting, setDeleting] = useState(false)
4243
const router = useRouter()
43-
const { stringifyParams } = useSearchParams()
44+
const { searchParams, stringifyParams } = useSearchParams()
4445
const { clearRouteCache } = useRouteCache()
4546

4647
const collectionPermissions = permissions?.collections?.[slug]
@@ -54,8 +55,16 @@ export const DeleteMany: React.FC<Props> = (props) => {
5455

5556
const handleDelete = useCallback(async () => {
5657
setDeleting(true)
58+
59+
const queryWithSearch = mergeListSearchAndWhere({
60+
collectionConfig: collection,
61+
search: searchParams?.search as string,
62+
})
63+
64+
const queryString = getQueryParams(queryWithSearch)
65+
5766
await requests
58-
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
67+
.delete(`${serverURL}${api}/${slug}${queryString}`, {
5968
headers: {
6069
'Accept-Language': i18n.language,
6170
'Content-Type': 'application/json',
@@ -107,6 +116,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
107116
}
108117
})
109118
}, [
119+
searchParams,
110120
addDefaultError,
111121
api,
112122
getQueryParams,
@@ -123,6 +133,7 @@ export const DeleteMany: React.FC<Props> = (props) => {
123133
toggleAll,
124134
toggleModal,
125135
clearRouteCache,
136+
collection,
126137
])
127138

128139
if (selectAll === SelectAllStatus.None || !hasDeletePermission) {

packages/ui/src/elements/EditMany/index.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ClientCollectionConfig, FormState } from 'payload'
44
import { useModal } from '@faceless-ui/modal'
55
import { getTranslation } from '@payloadcms/translations'
66
import { useRouter } from 'next/navigation.js'
7-
import React, { useCallback, useEffect, useState } from 'react'
7+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
88

99
import type { FormProps } from '../../forms/Form/index.js'
1010

@@ -24,9 +24,10 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j
2424
import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
2525
import { useTranslation } from '../../providers/Translation/index.js'
2626
import { abortAndIgnore } from '../../utilities/abortAndIgnore.js'
27+
import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js'
2728
import { Drawer, DrawerToggler } from '../Drawer/index.js'
28-
import { FieldSelect } from '../FieldSelect/index.js'
2929
import './index.scss'
30+
import { FieldSelect } from '../FieldSelect/index.js'
3031

3132
const baseClass = 'edit-many'
3233

@@ -124,7 +125,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
124125
const { count, getQueryParams, selectAll } = useSelection()
125126
const { i18n, t } = useTranslation()
126127
const [selected, setSelected] = useState([])
127-
const { stringifyParams } = useSearchParams()
128+
const { searchParams, stringifyParams } = useSearchParams()
128129
const router = useRouter()
129130
const [initialState, setInitialState] = useState<FormState>()
130131
const hasInitializedState = React.useRef(false)
@@ -191,9 +192,14 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
191192
}
192193
}, [])
193194

194-
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
195-
return null
196-
}
195+
const queryString = useMemo(() => {
196+
const queryWithSearch = mergeListSearchAndWhere({
197+
collectionConfig: collection,
198+
search: searchParams?.search as string,
199+
})
200+
201+
return getQueryParams(queryWithSearch)
202+
}, [collection, searchParams, getQueryParams])
197203

198204
const onSuccess = () => {
199205
router.replace(
@@ -205,6 +211,10 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
205211
closeModal(drawerSlug)
206212
}
207213

214+
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
215+
return null
216+
}
217+
208218
return (
209219
<div className={baseClass}>
210220
<DrawerToggler
@@ -272,17 +282,17 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
272282
{collection?.versions?.drafts ? (
273283
<React.Fragment>
274284
<SaveDraftButton
275-
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}&draft=true`}
285+
action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
276286
disabled={selected.length === 0}
277287
/>
278288
<PublishButton
279-
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}&draft=true`}
289+
action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
280290
disabled={selected.length === 0}
281291
/>
282292
</React.Fragment>
283293
) : (
284294
<Submit
285-
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}`}
295+
action={`${serverURL}${apiRoute}/${slug}${queryString}`}
286296
disabled={selected.length === 0}
287297
/>
288298
)}

packages/ui/src/providers/Selection/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
7575
}
7676
})
7777
}
78+
7879
setSelected(rows)
7980
},
8081
[docs, selectAll, user?.id],
@@ -107,8 +108,10 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
107108
const getQueryParams = useCallback(
108109
(additionalWhereParams?: Where): string => {
109110
let where: Where
111+
110112
if (selectAll === SelectAllStatus.AllAvailable) {
111113
const params = searchParams?.where as Where
114+
112115
where = params || {
113116
id: { not_equals: '' },
114117
}
@@ -127,11 +130,13 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
127130
},
128131
}
129132
}
133+
130134
if (additionalWhereParams) {
131135
where = {
132136
and: [{ ...additionalWhereParams }, where],
133137
}
134138
}
139+
135140
return qs.stringify(
136141
{
137142
locale,

packages/ui/src/utilities/mergeListSearchAndWhere.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export const hoistQueryParamsToAnd = (currentWhere: Where, incomingWhere: Where)
2929
type Args = {
3030
collectionConfig: ClientCollectionConfig | SanitizedCollectionConfig
3131
search: string
32-
where: Where
32+
where?: Where
3333
}
3434

35-
export const mergeListSearchAndWhere = ({ collectionConfig, search, where }: Args): Where => {
35+
export const mergeListSearchAndWhere = ({ collectionConfig, search, where = {} }: Args): Where => {
3636
if (search) {
3737
let copyOfWhere = { ...(where || {}) }
3838

test/_community/payload-types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ export interface Config {
4040
user: User & {
4141
collection: 'users';
4242
};
43-
jobs: {
43+
jobs?: {
4444
tasks: unknown;
45-
workflows: unknown;
45+
workflows?: unknown;
4646
};
4747
}
4848
export interface UserAuthOperations {

test/admin/e2e/3/e2e.spec.ts

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -452,38 +452,46 @@ describe('admin3', () => {
452452
expect(page.url()).toContain(postsUrl.list)
453453
})
454454

455-
test('should bulk delete', async () => {
456-
async function selectAndDeleteAll() {
457-
await page.goto(postsUrl.list)
458-
await page.locator('input#select-all').check()
459-
await page.locator('.delete-documents__toggle').click()
460-
await page.locator('#confirm-delete').click()
461-
}
462-
463-
// First, delete all posts created by the seed
455+
test('should bulk delete all on page', async () => {
464456
await deleteAllPosts()
465-
await createPost()
466-
await createPost()
467-
await createPost()
468-
457+
await Promise.all([createPost(), createPost(), createPost()])
469458
await page.goto(postsUrl.list)
470-
await selectAndDeleteAll()
459+
await page.locator('input#select-all').check()
460+
await page.locator('.delete-documents__toggle').click()
461+
await page.locator('#confirm-delete').click()
462+
471463
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
472464
'Deleted 3 Posts successfully.',
473465
)
466+
474467
await expect(page.locator('.collection-list__no-results')).toBeVisible()
475468
})
476469

470+
test('should bulk delete with filters and across pages', async () => {
471+
await deleteAllPosts()
472+
await Promise.all([createPost({ title: 'Post 1' }), createPost({ title: 'Post 2' })])
473+
await page.goto(postsUrl.list)
474+
await page.locator('#search-filter-input').fill('Post 1')
475+
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
476+
await page.locator('input#select-all').check()
477+
await page.locator('button.list-selection__button').click()
478+
await page.locator('.delete-documents__toggle').click()
479+
await page.locator('#confirm-delete').click()
480+
481+
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
482+
'Deleted 1 Post successfully.',
483+
)
484+
485+
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
486+
})
487+
477488
test('should bulk update', async () => {
478489
// First, delete all posts created by the seed
479490
await deleteAllPosts()
480-
await createPost()
481-
await createPost()
482-
await createPost()
483-
484-
const bulkTitle = 'Bulk update title'
491+
const post1Title = 'Post'
492+
const updatedPostTitle = `${post1Title} (Updated)`
493+
await Promise.all([createPost({ title: post1Title }), createPost(), createPost()])
485494
await page.goto(postsUrl.list)
486-
487495
await page.locator('input#select-all').check()
488496
await page.locator('.edit-many__toggle').click()
489497
await page.locator('.field-select .rs__control').click()
@@ -493,21 +501,52 @@ describe('admin3', () => {
493501
})
494502

495503
await expect(titleOption).toBeVisible()
496-
497504
await titleOption.click()
498505
const titleInput = page.locator('#field-title')
499-
500506
await expect(titleInput).toBeVisible()
507+
await titleInput.fill(updatedPostTitle)
508+
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
501509

502-
await titleInput.fill(bulkTitle)
510+
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
511+
'Updated 3 Posts successfully.',
512+
)
513+
514+
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
515+
await expect(page.locator('.row-2 .cell-title')).toContainText(updatedPostTitle)
516+
await expect(page.locator('.row-3 .cell-title')).toContainText(updatedPostTitle)
517+
})
518+
519+
test('should bulk update with filters and across pages', async () => {
520+
// First, delete all posts created by the seed
521+
await deleteAllPosts()
522+
const post1Title = 'Post 1'
523+
await Promise.all([createPost({ title: post1Title }), createPost({ title: 'Post 2' })])
524+
const updatedPostTitle = `${post1Title} (Updated)`
525+
await page.goto(postsUrl.list)
526+
await page.locator('#search-filter-input').fill('Post 1')
527+
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
528+
await page.locator('input#select-all').check()
529+
await page.locator('button.list-selection__button').click()
530+
await page.locator('.edit-many__toggle').click()
531+
await page.locator('.field-select .rs__control').click()
532+
533+
const titleOption = page.locator('.field-select .rs__option', {
534+
hasText: exactText('Title'),
535+
})
536+
537+
await expect(titleOption).toBeVisible()
538+
await titleOption.click()
539+
const titleInput = page.locator('#field-title')
540+
await expect(titleInput).toBeVisible()
541+
await titleInput.fill(updatedPostTitle)
503542

504543
await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
505544
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
506-
'Updated 3 Posts successfully.',
545+
'Updated 1 Post successfully.',
507546
)
508-
await expect(page.locator('.row-1 .cell-title')).toContainText(bulkTitle)
509-
await expect(page.locator('.row-2 .cell-title')).toContainText(bulkTitle)
510-
await expect(page.locator('.row-3 .cell-title')).toContainText(bulkTitle)
547+
548+
await expect(page.locator('.table table > tbody > tr')).toHaveCount(1)
549+
await expect(page.locator('.row-1 .cell-title')).toContainText(updatedPostTitle)
511550
})
512551

513552
test('should save globals', async () => {

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
],
3838
"paths": {
3939
"@payload-config": [
40-
"./test/versions/config.ts"
40+
"./test/_community/config.ts"
4141
],
4242
"@payloadcms/live-preview": [
4343
"./packages/live-preview/src"

0 commit comments

Comments
 (0)