From ef3748319ebff813199e62b0a7dfb261388aee3a Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Wed, 20 Nov 2024 13:22:43 -0500 Subject: [PATCH] fix(ui): bulk update and delete ignoring search query (#9377) Fixes #9374. --- packages/ui/src/elements/DeleteMany/index.tsx | 19 +++- packages/ui/src/elements/EditMany/index.tsx | 28 ++++-- packages/ui/src/providers/Selection/index.tsx | 5 + .../src/utilities/mergeListSearchAndWhere.ts | 4 +- test/_community/payload-types.ts | 4 +- test/admin/e2e/3/e2e.spec.ts | 93 +++++++++++++------ tsconfig.json | 2 +- 7 files changed, 110 insertions(+), 45 deletions(-) diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index 4df7ff83412..c66b47cb8f3 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -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) => { - 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) => { 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) => { 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) => { } }) }, [ + searchParams, addDefaultError, api, getQueryParams, @@ -123,6 +133,7 @@ export const DeleteMany: React.FC = (props) => { toggleAll, toggleModal, clearRouteCache, + collection, ]) if (selectAll === SelectAllStatus.None || !hasDeletePermission) { diff --git a/packages/ui/src/elements/EditMany/index.tsx b/packages/ui/src/elements/EditMany/index.tsx index a765ddedcb1..f2c82c8cf32 100644 --- a/packages/ui/src/elements/EditMany/index.tsx +++ b/packages/ui/src/elements/EditMany/index.tsx @@ -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 = (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() const hasInitializedState = React.useRef(false) @@ -191,9 +192,14 @@ export const EditMany: React.FC = (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 = (props) => { closeModal(drawerSlug) } + if (selectAll === SelectAllStatus.None || !hasUpdatePermission) { + return null + } + return (
= (props) => { {collection?.versions?.drafts ? ( ) : ( )} diff --git a/packages/ui/src/providers/Selection/index.tsx b/packages/ui/src/providers/Selection/index.tsx index 9696c0e63c0..1e65de9461d 100644 --- a/packages/ui/src/providers/Selection/index.tsx +++ b/packages/ui/src/providers/Selection/index.tsx @@ -75,6 +75,7 @@ export const SelectionProvider: React.FC = ({ children, docs = [], totalD } }) } + setSelected(rows) }, [docs, selectAll, user?.id], @@ -107,8 +108,10 @@ export const SelectionProvider: React.FC = ({ 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 = ({ children, docs = [], totalD }, } } + if (additionalWhereParams) { where = { and: [{ ...additionalWhereParams }, where], } } + return qs.stringify( { locale, diff --git a/packages/ui/src/utilities/mergeListSearchAndWhere.ts b/packages/ui/src/utilities/mergeListSearchAndWhere.ts index 32b7639d91f..7e9ad5ba0ee 100644 --- a/packages/ui/src/utilities/mergeListSearchAndWhere.ts +++ b/packages/ui/src/utilities/mergeListSearchAndWhere.ts @@ -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 || {}) } diff --git a/test/_community/payload-types.ts b/test/_community/payload-types.ts index fd6483846a1..129bd05dd78 100644 --- a/test/_community/payload-types.ts +++ b/test/_community/payload-types.ts @@ -40,9 +40,9 @@ export interface Config { user: User & { collection: 'users'; }; - jobs: { + jobs?: { tasks: unknown; - workflows: unknown; + workflows?: unknown; }; } export interface UserAuthOperations { diff --git a/test/admin/e2e/3/e2e.spec.ts b/test/admin/e2e/3/e2e.spec.ts index 8cc4fa7f724..edbc062e7b2 100644 --- a/test/admin/e2e/3/e2e.spec.ts +++ b/test/admin/e2e/3/e2e.spec.ts @@ -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() @@ -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 () => { diff --git a/tsconfig.json b/tsconfig.json index ac5eff74319..314fd332c39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,7 @@ ], "paths": { "@payload-config": [ - "./test/versions/config.ts" + "./test/_community/config.ts" ], "@payloadcms/live-preview": [ "./packages/live-preview/src"