Skip to content

Commit bd8ced1

Browse files
authored
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> ) } ```
1 parent 1328522 commit bd8ced1

File tree

41 files changed

+832
-1377
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+832
-1377
lines changed

packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.scss

Lines changed: 0 additions & 40 deletions
This file was deleted.

packages/next/src/views/Account/ResetPreferences/ConfirmResetModal/index.tsx

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
'use client'
2+
import type { OnConfirm } from '@payloadcms/ui'
23
import type { User } from 'payload'
34

4-
import { Button, LoadingOverlay, toast, useModal, useTranslation } from '@payloadcms/ui'
5+
import { Button, ConfirmationModal, toast, useModal, useTranslation } from '@payloadcms/ui'
56
import * as qs from 'qs-esm'
6-
import { Fragment, useCallback, useState } from 'react'
7-
8-
import { ConfirmResetModal } from './ConfirmResetModal/index.js'
7+
import { Fragment, useCallback } from 'react'
98

109
const confirmResetModalSlug = 'confirm-reset-modal'
1110

@@ -16,51 +15,54 @@ export const ResetPreferences: React.FC<{
1615
const { openModal } = useModal()
1716
const { t } = useTranslation()
1817

19-
const [loading, setLoading] = useState(false)
20-
21-
const handleResetPreferences = useCallback(async () => {
22-
if (!user || loading) {
23-
return
24-
}
25-
setLoading(true)
18+
const handleResetPreferences: OnConfirm = useCallback(
19+
async ({ closeConfirmationModal, setConfirming }) => {
20+
if (!user) {
21+
setConfirming(false)
22+
closeConfirmationModal()
23+
return
24+
}
2625

27-
const stringifiedQuery = qs.stringify(
28-
{
29-
depth: 0,
30-
where: {
31-
user: {
32-
id: {
33-
equals: user.id,
26+
const stringifiedQuery = qs.stringify(
27+
{
28+
depth: 0,
29+
where: {
30+
user: {
31+
id: {
32+
equals: user.id,
33+
},
3434
},
3535
},
3636
},
37-
},
38-
{ addQueryPrefix: true },
39-
)
37+
{ addQueryPrefix: true },
38+
)
4039

41-
try {
42-
const res = await fetch(`${apiRoute}/payload-preferences${stringifiedQuery}`, {
43-
credentials: 'include',
44-
headers: {
45-
'Content-Type': 'application/json',
46-
},
47-
method: 'DELETE',
48-
})
40+
try {
41+
const res = await fetch(`${apiRoute}/payload-preferences${stringifiedQuery}`, {
42+
credentials: 'include',
43+
headers: {
44+
'Content-Type': 'application/json',
45+
},
46+
method: 'DELETE',
47+
})
4948

50-
const json = await res.json()
51-
const message = json.message
49+
const json = await res.json()
50+
const message = json.message
5251

53-
if (res.ok) {
54-
toast.success(message)
55-
} else {
56-
toast.error(message)
52+
if (res.ok) {
53+
toast.success(message)
54+
} else {
55+
toast.error(message)
56+
}
57+
} catch (_err) {
58+
// swallow error
59+
} finally {
60+
setConfirming(false)
61+
closeConfirmationModal()
5762
}
58-
} catch (e) {
59-
// swallow error
60-
} finally {
61-
setLoading(false)
62-
}
63-
}, [apiRoute, loading, user])
63+
},
64+
[apiRoute, user],
65+
)
6466

6567
return (
6668
<Fragment>
@@ -69,8 +71,13 @@ export const ResetPreferences: React.FC<{
6971
{t('general:resetPreferences')}
7072
</Button>
7173
</div>
72-
<ConfirmResetModal onConfirm={handleResetPreferences} slug={confirmResetModalSlug} />
73-
{loading && <LoadingOverlay loadingText={t('general:resettingPreferences')} />}
74+
<ConfirmationModal
75+
body={t('general:resetPreferencesDescription')}
76+
confirmingLabel={t('general:resettingPreferences')}
77+
heading={t('general:resetPreferences')}
78+
modalSlug={confirmResetModalSlug}
79+
onConfirm={handleResetPreferences}
80+
/>
7481
</Fragment>
7582
)
7683
}

packages/next/src/views/Version/Restore/index.tsx

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
'use client'
2+
import type { OnConfirm } from '@payloadcms/ui'
3+
24
import { getTranslation } from '@payloadcms/translations'
35
import {
46
Button,
5-
ChevronIcon,
6-
Modal,
7-
Pill,
8-
Popup,
7+
ConfirmationModal,
98
PopupList,
109
useConfig,
1110
useModal,
@@ -45,7 +44,6 @@ const Restore: React.FC<Props> = ({
4544
const collectionConfig = getEntityConfig({ collectionSlug })
4645

4746
const { toggleModal } = useModal()
48-
const [processing, setProcessing] = useState(false)
4947
const router = useRouter()
5048
const { i18n, t } = useTranslation()
5149
const [draft, setDraft] = useState(false)
@@ -77,23 +75,27 @@ const Restore: React.FC<Props> = ({
7775
})
7876
}
7977

80-
const handleRestore = useCallback(async () => {
81-
setProcessing(true)
82-
83-
const res = await requests.post(fetchURL, {
84-
headers: {
85-
'Accept-Language': i18n.language,
86-
},
87-
})
88-
89-
if (res.status === 200) {
90-
const json = await res.json()
91-
toast.success(json.message)
92-
startRouteTransition(() => router.push(redirectURL))
93-
} else {
94-
toast.error(t('version:problemRestoringVersion'))
95-
}
96-
}, [fetchURL, redirectURL, t, i18n, router, startRouteTransition])
78+
const handleRestore: OnConfirm = useCallback(
79+
async ({ closeConfirmationModal, setConfirming }) => {
80+
const res = await requests.post(fetchURL, {
81+
headers: {
82+
'Accept-Language': i18n.language,
83+
},
84+
})
85+
86+
setConfirming(false)
87+
closeConfirmationModal()
88+
89+
if (res.status === 200) {
90+
const json = await res.json()
91+
toast.success(json.message)
92+
startRouteTransition(() => router.push(redirectURL))
93+
} else {
94+
toast.error(t('version:problemRestoringVersion'))
95+
}
96+
},
97+
[fetchURL, redirectURL, t, i18n, router, startRouteTransition],
98+
)
9799

98100
return (
99101
<Fragment>
@@ -118,27 +120,13 @@ const Restore: React.FC<Props> = ({
118120
{t('version:restoreThisVersion')}
119121
</Button>
120122
</div>
121-
<Modal className={`${baseClass}__modal`} slug={modalSlug}>
122-
<div className={`${baseClass}__wrapper`}>
123-
<div className={`${baseClass}__content`}>
124-
<h1>{t('version:confirmVersionRestoration')}</h1>
125-
<p>{restoreMessage}</p>
126-
</div>
127-
<div className={`${baseClass}__controls`}>
128-
<Button
129-
buttonStyle="secondary"
130-
onClick={processing ? undefined : () => toggleModal(modalSlug)}
131-
size="large"
132-
type="button"
133-
>
134-
{t('general:cancel')}
135-
</Button>
136-
<Button onClick={processing ? undefined : () => void handleRestore()}>
137-
{processing ? t('version:restoring') : t('general:confirm')}
138-
</Button>
139-
</div>
140-
</div>
141-
</Modal>
123+
<ConfirmationModal
124+
body={restoreMessage}
125+
confirmingLabel={t('version:restoring')}
126+
heading={t('version:confirmVersionRestoration')}
127+
modalSlug={modalSlug}
128+
onConfirm={handleRestore}
129+
/>
142130
</Fragment>
143131
)
144132
}

packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.scss

Lines changed: 0 additions & 39 deletions
This file was deleted.

packages/plugin-search/src/Search/ui/ReindexButton/ReindexConfirmModal/index.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)