feat(ui): confirmation modal (#11271)

There are nearly a dozen independent implementations of the same modal
spread throughout the admin panel and various plugins. These modals are
used to confirm or cancel an action, such as deleting a document, bulk
publishing, etc. Each of these instances is nearly identical, leading to
unnecessary development efforts when creating them, inconsistent UI, and
duplicative stylesheets.

Everything is now standardized behind a new `ConfirmationModal`
component. This modal comes with a standard API that is flexible enough
to replace nearly every instance. This component has also been exported
for reuse.

Here is a basic example of how to use it:

```tsx
'use client'
import { ConfirmationModal, useModal } from '@payloadcms/ui'
import React, { Fragment } from 'react'

const modalSlug = 'my-confirmation-modal'

export function MyComponent() {
  const { openModal } = useModal()

  return (
    <Fragment>
      <button
        onClick={() => {
          openModal(modalSlug)
        }}
        type="button"
      >
        Do something
      </button>
      <ConfirmationModal
        heading="Are you sure?"
        body="Confirm or cancel before proceeding."
        modalSlug={modalSlug}
        onConfirm={({ closeConfirmationModal, setConfirming }) => {
          // do something
          setConfirming(false)
          closeConfirmationModal()
        }}
      />
    </Fragment>
  )
}
```
This commit is contained in:
Jacob Fletcher
2025-02-19 02:27:03 -05:00
committed by GitHub
parent 132852290a
commit bd8ced1b60
41 changed files with 819 additions and 1364 deletions

View File

@@ -704,7 +704,7 @@ describe('General', () => {
await page.goto(postsUrl.edit(id))
await openDocControls(page)
await page.locator('#action-delete').click()
await page.locator('#confirm-delete').click()
await page.locator(`[id=delete-${id}] #confirm-action`).click()
await expect(page.locator(`text=Post "${title}" successfully deleted.`)).toBeVisible()
expect(page.url()).toContain(postsUrl.list)
})
@@ -715,7 +715,7 @@ describe('General', () => {
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()
await page.locator('#delete-posts #confirm-action').click()
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
'Deleted 3 Posts successfully.',
@@ -733,7 +733,7 @@ describe('General', () => {
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 page.locator('#delete-posts #confirm-action').click()
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
'Deleted 1 Post successfully.',
@@ -917,7 +917,9 @@ describe('General', () => {
await expect(modalContainer).toBeVisible()
// Click the "Leave anyway" button
await page.locator('.leave-without-saving__controls .btn--style-primary').click()
await page
.locator('#leave-without-saving .confirmation-modal__controls .btn--style-primary')
.click()
// Assert that the class on the modal container changes to 'payload__modal-container--exitDone'
await expect(modalContainer).toHaveClass(/payload__modal-container--exitDone/)

View File

@@ -1006,19 +1006,18 @@ describe('List View', () => {
test('should delete many', async () => {
await page.goto(postsUrl.list)
await page.waitForURL(new RegExp(postsUrl.list))
// delete should not appear without selection
await expect(page.locator('#confirm-delete')).toHaveCount(0)
await expect(page.locator('#delete-posts #confirm-action')).toHaveCount(0)
// select one row
await page.locator('.row-1 .cell-_select input').check()
// delete button should be present
await expect(page.locator('#confirm-delete')).toHaveCount(1)
await expect(page.locator('#delete-posts #confirm-action')).toHaveCount(1)
await page.locator('.row-2 .cell-_select input').check()
await page.locator('.delete-documents__toggle').click()
await page.locator('#confirm-delete').click()
await page.locator('#delete-posts #confirm-action').click()
await expect(page.locator('.cell-_select')).toHaveCount(1)
})
})

View File

@@ -64,6 +64,7 @@ export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
uploads: Upload;
posts: Post;