diff --git a/packages/ui/src/elements/EditMany/DrawerContent.tsx b/packages/ui/src/elements/EditMany/DrawerContent.tsx index 5306375008..74b6c50ff3 100644 --- a/packages/ui/src/elements/EditMany/DrawerContent.tsx +++ b/packages/ui/src/elements/EditMany/DrawerContent.tsx @@ -29,9 +29,9 @@ import { useTranslation } from '../../providers/Translation/index.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { parseSearchParams } from '../../utilities/parseSearchParams.js' import { FieldSelect } from '../FieldSelect/index.js' -import { baseClass, type EditManyProps } from './index.js' import './index.scss' import '../../forms/RenderFields/index.scss' +import { baseClass, type EditManyProps } from './index.js' const Submit: React.FC<{ readonly action: string @@ -123,6 +123,10 @@ type EditManyDrawerContentProps = { * The IDs of the selected items */ ids?: (number | string)[] + /** + * The function to call after a successful action + */ + onSuccess?: () => void /** * Whether all items are selected */ @@ -143,6 +147,7 @@ export const EditManyDrawerContent: React.FC = (prop count, drawerSlug, ids, + onSuccess: onSuccessFromProps, selectAll, selectedFields, setSelectedFields, @@ -263,6 +268,10 @@ export const EditManyDrawerContent: React.FC = (prop ) clearRouteCache() closeModal(drawerSlug) + + if (typeof onSuccessFromProps === 'function') { + onSuccessFromProps() + } } const onFieldSelect = useCallback( diff --git a/packages/ui/src/elements/EditMany/index.tsx b/packages/ui/src/elements/EditMany/index.tsx index a4172840f9..b9aa3a11ae 100644 --- a/packages/ui/src/elements/EditMany/index.tsx +++ b/packages/ui/src/elements/EditMany/index.tsx @@ -22,12 +22,13 @@ export type EditManyProps = { } export const EditMany: React.FC = (props) => { - const { count, selectAll, selected } = useSelection() + const { count, selectAll, selected, toggleAll } = useSelection() return ( toggleAll(false)} selectAll={selectAll === SelectAllStatus.AllAvailable} /> ) @@ -37,9 +38,10 @@ export const EditMany_v4: React.FC< { count: number ids: (number | string)[] + onSuccess?: () => void selectAll: boolean } & EditManyProps -> = ({ collection, count, ids, selectAll }) => { +> = ({ collection, count, ids, onSuccess, selectAll }) => { const { permissions } = useAuth() const { openModal } = useModal() @@ -73,6 +75,7 @@ export const EditMany_v4: React.FC< count={count} drawerSlug={drawerSlug} ids={ids} + onSuccess={onSuccess} selectAll={selectAll} selectedFields={selectedFields} setSelectedFields={setSelectedFields} diff --git a/packages/ui/src/elements/PublishMany/DrawerContent.tsx b/packages/ui/src/elements/PublishMany/DrawerContent.tsx index b818144497..7d81e3dbdf 100644 --- a/packages/ui/src/elements/PublishMany/DrawerContent.tsx +++ b/packages/ui/src/elements/PublishMany/DrawerContent.tsx @@ -20,6 +20,7 @@ import { ConfirmationModal } from '../ConfirmationModal/index.js' type PublishManyDrawerContentProps = { drawerSlug: string ids: (number | string)[] + onSuccess?: () => void selectAll: boolean } & PublishManyProps export function PublishManyDrawerContent(props: PublishManyDrawerContentProps) { @@ -28,6 +29,7 @@ export function PublishManyDrawerContent(props: PublishManyDrawerContentProps) { collection: { slug, labels: { plural, singular } } = {}, drawerSlug, ids, + onSuccess, selectAll, } = props @@ -136,6 +138,11 @@ export function PublishManyDrawerContent(props: PublishManyDrawerContentProps) { ) clearRouteCache() + + if (typeof onSuccess === 'function') { + onSuccess() + } + return null } @@ -163,6 +170,7 @@ export function PublishManyDrawerContent(props: PublishManyDrawerContentProps) { selectAll, clearRouteCache, addDefaultError, + onSuccess, ]) return ( diff --git a/packages/ui/src/elements/PublishMany/index.tsx b/packages/ui/src/elements/PublishMany/index.tsx index 351ee03965..0df29b1856 100644 --- a/packages/ui/src/elements/PublishMany/index.tsx +++ b/packages/ui/src/elements/PublishMany/index.tsx @@ -15,13 +15,14 @@ export type PublishManyProps = { } export const PublishMany: React.FC = (props) => { - const { count, selectAll, selected } = useSelection() + const { count, selectAll, selected, toggleAll } = useSelection() return ( toggleAll(false)} selectAll={selectAll === SelectAllStatus.AllAvailable} /> ) @@ -30,10 +31,18 @@ export const PublishMany: React.FC = (props) => { type PublishMany_v4Props = { count: number ids: (number | string)[] + onSuccess?: () => void selectAll: boolean } & PublishManyProps export const PublishMany_v4: React.FC = (props) => { - const { collection, collection: { slug, versions } = {}, count, ids, selectAll } = props + const { + collection, + collection: { slug, versions } = {}, + count, + ids, + onSuccess, + selectAll, + } = props const { permissions } = useAuth() const { t } = useTranslation() @@ -63,6 +72,7 @@ export const PublishMany_v4: React.FC = (props) => { collection={collection} drawerSlug={drawerSlug} ids={ids} + onSuccess={onSuccess} selectAll={selectAll} /> diff --git a/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx b/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx index 6c6f2fafa4..bf7ead0a29 100644 --- a/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx +++ b/packages/ui/src/elements/UnpublishMany/DrawerContent.tsx @@ -20,6 +20,7 @@ import { ConfirmationModal } from '../ConfirmationModal/index.js' type UnpublishManyDrawerContentProps = { drawerSlug: string ids: (number | string)[] + onSuccess?: () => void selectAll: boolean } & UnpublishManyProps @@ -29,6 +30,7 @@ export function UnpublishManyDrawerContent(props: UnpublishManyDrawerContentProp collection: { slug, labels: { plural, singular } } = {}, drawerSlug, ids, + onSuccess, selectAll, } = props @@ -126,6 +128,11 @@ export function UnpublishManyDrawerContent(props: UnpublishManyDrawerContentProp ) clearRouteCache() + + if (typeof onSuccess === 'function') { + onSuccess() + } + return null } @@ -153,6 +160,7 @@ export function UnpublishManyDrawerContent(props: UnpublishManyDrawerContentProp selectAll, clearRouteCache, addDefaultError, + onSuccess, ]) return ( diff --git a/packages/ui/src/elements/UnpublishMany/index.tsx b/packages/ui/src/elements/UnpublishMany/index.tsx index a4df78ee30..b59a955f9f 100644 --- a/packages/ui/src/elements/UnpublishMany/index.tsx +++ b/packages/ui/src/elements/UnpublishMany/index.tsx @@ -15,13 +15,14 @@ export type UnpublishManyProps = { } export const UnpublishMany: React.FC = (props) => { - const { count, selectAll, selected } = useSelection() + const { count, selectAll, selected, toggleAll } = useSelection() return ( toggleAll(false)} selectAll={selectAll === SelectAllStatus.AllAvailable} /> ) @@ -31,10 +32,18 @@ export const UnpublishMany_v4: React.FC< { count: number ids: (number | string)[] + onSuccess?: () => void selectAll: boolean } & UnpublishManyProps > = (props) => { - const { collection, collection: { slug, versions } = {}, count, ids, selectAll } = props + const { + collection, + collection: { slug, versions } = {}, + count, + ids, + onSuccess, + selectAll, + } = props const { t } = useTranslation() const { permissions } = useAuth() @@ -63,6 +72,7 @@ export const UnpublishMany_v4: React.FC< collection={collection} drawerSlug={drawerSlug} ids={ids} + onSuccess={onSuccess} selectAll={selectAll} /> diff --git a/packages/ui/src/views/List/ListSelection/index.tsx b/packages/ui/src/views/List/ListSelection/index.tsx index 4b6a24748c..f87cea25d9 100644 --- a/packages/ui/src/views/List/ListSelection/index.tsx +++ b/packages/ui/src/views/List/ListSelection/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { ClientCollectionConfig } from 'payload' -import React, { Fragment } from 'react' +import React, { Fragment, useCallback } from 'react' import { DeleteMany } from '../../../elements/DeleteMany/index.js' import { EditMany_v4 } from '../../../elements/EditMany/index.js' @@ -27,6 +27,8 @@ export const ListSelection: React.FC = ({ const { count, getSelectedIds, selectAll, toggleAll, totalDocs } = useSelection() const { t } = useTranslation() + const onActionSuccess = useCallback(() => toggleAll(false), [toggleAll]) + if (count === 0) { return null } @@ -53,18 +55,21 @@ export const ListSelection: React.FC = ({ collection={collectionConfig} count={count} ids={getSelectedIds()} + onSuccess={onActionSuccess} selectAll={selectAll === SelectAllStatus.AllAvailable} /> diff --git a/test/bulk-edit/e2e.spec.ts b/test/bulk-edit/e2e.spec.ts index 77aee50d79..3866c13440 100644 --- a/test/bulk-edit/e2e.spec.ts +++ b/test/bulk-edit/e2e.spec.ts @@ -488,6 +488,112 @@ test.describe('Bulk Edit', () => { await expect(field.locator('#field-array__0__noRead')).toBeHidden() await expect(field.locator('#field-array__0__noUpdate')).toBeDisabled() }) + + test('should toggle list selections off on successful publish', async () => { + await deleteAllPosts() + + const postCount = 3 + Array.from({ length: postCount }).forEach(async (_, i) => { + await createPost({ title: `Post ${i + 1}` }, { draft: true }) + }) + + await page.goto(postsUrl.list) + await page.locator('input#select-all').check() + + await page.locator('.list-selection__button[aria-label="Publish"]').click() + await page.locator('#publish-posts #confirm-action').click() + + await expect(page.locator('.payload-toast-container .toast-success')).toContainText( + `Updated ${postCount} Posts successfully.`, + ) + + // eslint-disable-next-line jest-dom/prefer-checked + await expect(page.locator('input#select-all')).not.toHaveAttribute('checked', '') + + for (let i = 0; i < postCount; i++) { + // eslint-disable-next-line jest-dom/prefer-checked + await expect(findTableCell(page, '_select', `Post ${i + 1}`)).not.toHaveAttribute( + 'checked', + '', + ) + } + }) + + test('should toggle list selections off on successful unpublish', async () => { + await deleteAllPosts() + + const postCount = 3 + Array.from({ length: postCount }).forEach(async (_, i) => { + await createPost({ title: `Post ${i + 1}`, _status: 'published' }) + }) + + await page.goto(postsUrl.list) + await page.locator('input#select-all').check() + + await page.locator('.list-selection__button[aria-label="Unpublish"]').click() + await page.locator('#unpublish-posts #confirm-action').click() + + await expect(page.locator('.payload-toast-container .toast-success')).toContainText( + `Updated ${postCount} Posts successfully.`, + ) + + // eslint-disable-next-line jest-dom/prefer-checked + await expect(page.locator('input#select-all')).not.toHaveAttribute('checked', '') + + for (let i = 0; i < postCount; i++) { + // eslint-disable-next-line jest-dom/prefer-checked + await expect(findTableCell(page, '_select', `Post ${i + 1}`)).not.toHaveAttribute( + 'checked', + '', + ) + } + }) + + test('should toggle list selections off on successful edit', async () => { + await deleteAllPosts() + + const postCount = 3 + Array.from({ length: postCount }).forEach(async (_, i) => { + await createPost({ title: `Post ${i + 1}` }) + }) + + await page.goto(postsUrl.list) + await page.locator('input#select-all').check() + + await page.locator('.list-selection__button[aria-label="Edit"]').click() + + const editDrawer = page.locator('dialog#edit-posts') + await expect(editDrawer).toBeVisible() + + const fieldSelect = editDrawer.locator('.field-select') + await expect(fieldSelect).toBeVisible() + + const fieldSelectControl = fieldSelect.locator('.rs__control') + await expect(fieldSelectControl).toBeVisible() + await fieldSelectControl.click() + + const titleOption = fieldSelect.locator('.rs__option:has-text("Title")').first() + await titleOption.click() + + await editDrawer.locator('input#field-title').fill('test') + + await editDrawer.locator('button[type="submit"]:has-text("Publish changes")').click() + + await expect(page.locator('.payload-toast-container .toast-success')).toContainText( + `Updated ${postCount} Posts successfully.`, + ) + + // eslint-disable-next-line jest-dom/prefer-checked + await expect(page.locator('input#select-all')).not.toHaveAttribute('checked', '') + + for (let i = 0; i < postCount; i++) { + // eslint-disable-next-line jest-dom/prefer-checked + await expect(findTableCell(page, '_select', `Post ${i + 1}`)).not.toHaveAttribute( + 'checked', + '', + ) + } + }) }) async function selectFieldToEdit(