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:
@@ -1,40 +0,0 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.reset-preferences-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(2);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(1);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
import { Button, Modal, useModal, useTranslation } from '@payloadcms/ui'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'reset-preferences-modal'
|
||||
|
||||
export const ConfirmResetModal: React.FC<{
|
||||
readonly onConfirm: () => void
|
||||
readonly slug: string
|
||||
}> = ({ slug, onConfirm }) => {
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClose = () => closeModal(slug)
|
||||
|
||||
const handleConfirm = () => {
|
||||
handleClose()
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal className={baseClass} slug={slug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:resetPreferences')}?</h1>
|
||||
<p>{t('general:resetPreferencesDescription')}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button buttonStyle="secondary" onClick={handleClose} size="large">
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} size="large">
|
||||
{t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client'
|
||||
import type { OnConfirm } from '@payloadcms/ui'
|
||||
import type { User } from 'payload'
|
||||
|
||||
import { Button, LoadingOverlay, toast, useModal, useTranslation } from '@payloadcms/ui'
|
||||
import { Button, ConfirmationModal, toast, useModal, useTranslation } from '@payloadcms/ui'
|
||||
import * as qs from 'qs-esm'
|
||||
import { Fragment, useCallback, useState } from 'react'
|
||||
|
||||
import { ConfirmResetModal } from './ConfirmResetModal/index.js'
|
||||
import { Fragment, useCallback } from 'react'
|
||||
|
||||
const confirmResetModalSlug = 'confirm-reset-modal'
|
||||
|
||||
@@ -16,13 +15,13 @@ export const ResetPreferences: React.FC<{
|
||||
const { openModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleResetPreferences = useCallback(async () => {
|
||||
if (!user || loading) {
|
||||
const handleResetPreferences: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
if (!user) {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
|
||||
const stringifiedQuery = qs.stringify(
|
||||
{
|
||||
@@ -55,12 +54,15 @@ export const ResetPreferences: React.FC<{
|
||||
} else {
|
||||
toast.error(message)
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
}
|
||||
}, [apiRoute, loading, user])
|
||||
},
|
||||
[apiRoute, user],
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -69,8 +71,13 @@ export const ResetPreferences: React.FC<{
|
||||
{t('general:resetPreferences')}
|
||||
</Button>
|
||||
</div>
|
||||
<ConfirmResetModal onConfirm={handleResetPreferences} slug={confirmResetModalSlug} />
|
||||
{loading && <LoadingOverlay loadingText={t('general:resettingPreferences')} />}
|
||||
<ConfirmationModal
|
||||
body={t('general:resetPreferencesDescription')}
|
||||
confirmingLabel={t('general:resettingPreferences')}
|
||||
heading={t('general:resetPreferences')}
|
||||
modalSlug={confirmResetModalSlug}
|
||||
onConfirm={handleResetPreferences}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client'
|
||||
import type { OnConfirm } from '@payloadcms/ui'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
Button,
|
||||
ChevronIcon,
|
||||
Modal,
|
||||
Pill,
|
||||
Popup,
|
||||
ConfirmationModal,
|
||||
PopupList,
|
||||
useConfig,
|
||||
useModal,
|
||||
@@ -45,7 +44,6 @@ const Restore: React.FC<Props> = ({
|
||||
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||
|
||||
const { toggleModal } = useModal()
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const router = useRouter()
|
||||
const { i18n, t } = useTranslation()
|
||||
const [draft, setDraft] = useState(false)
|
||||
@@ -77,15 +75,17 @@ const Restore: React.FC<Props> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
setProcessing(true)
|
||||
|
||||
const handleRestore: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
const res = await requests.post(fetchURL, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
})
|
||||
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
if (res.status === 200) {
|
||||
const json = await res.json()
|
||||
toast.success(json.message)
|
||||
@@ -93,7 +93,9 @@ const Restore: React.FC<Props> = ({
|
||||
} else {
|
||||
toast.error(t('version:problemRestoringVersion'))
|
||||
}
|
||||
}, [fetchURL, redirectURL, t, i18n, router, startRouteTransition])
|
||||
},
|
||||
[fetchURL, redirectURL, t, i18n, router, startRouteTransition],
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -118,27 +120,13 @@ const Restore: React.FC<Props> = ({
|
||||
{t('version:restoreThisVersion')}
|
||||
</Button>
|
||||
</div>
|
||||
<Modal className={`${baseClass}__modal`} slug={modalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('version:confirmVersionRestoration')}</h1>
|
||||
<p>{restoreMessage}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={processing ? undefined : () => toggleModal(modalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button onClick={processing ? undefined : () => void handleRestore()}>
|
||||
{processing ? t('version:restoring') : t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
body={restoreMessage}
|
||||
confirmingLabel={t('version:restoring')}
|
||||
heading={t('version:confirmVersionRestoration')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={handleRestore}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
@import '~@payloadcms/ui/scss';
|
||||
|
||||
@layer payload-default {
|
||||
.reindex-confirm-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(2);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(1);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Button, Modal, useTranslation } from '@payloadcms/ui'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
type Props = {
|
||||
description: string
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const baseClass = 'reindex-confirm-modal'
|
||||
|
||||
export const ReindexConfirmModal = ({ slug, description, onCancel, onConfirm, title }: Props) => {
|
||||
const {
|
||||
i18n: { t },
|
||||
} = useTranslation()
|
||||
return (
|
||||
<Modal className={baseClass} slug={slug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button buttonStyle="secondary" onClick={onCancel} size="large">
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} size="large">
|
||||
{t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { OnConfirm } from '@payloadcms/ui'
|
||||
|
||||
import {
|
||||
LoadingOverlay,
|
||||
ConfirmationModal,
|
||||
Popup,
|
||||
PopupList,
|
||||
toast,
|
||||
@@ -16,7 +18,6 @@ import React, { useCallback, useMemo, useState } from 'react'
|
||||
import type { ReindexButtonProps } from './types.js'
|
||||
|
||||
import { ReindexButtonLabel } from './ReindexButtonLabel/index.js'
|
||||
import { ReindexConfirmModal } from './ReindexConfirmModal/index.js'
|
||||
|
||||
const confirmReindexModalSlug = 'confirm-reindex-modal'
|
||||
|
||||
@@ -37,21 +38,19 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
const router = useRouter()
|
||||
|
||||
const [reindexCollections, setReindexCollections] = useState<string[]>([])
|
||||
const [isLoading, setLoading] = useState<boolean>(false)
|
||||
|
||||
const openConfirmModal = useCallback(() => openModal(confirmReindexModalSlug), [openModal])
|
||||
const closeConfirmModal = useCallback(() => closeModal(confirmReindexModalSlug), [closeModal])
|
||||
|
||||
const handleReindexSubmit = useCallback(async () => {
|
||||
if (isLoading || !reindexCollections.length) {
|
||||
const handleReindexSubmit: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
if (!reindexCollections.length) {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
return
|
||||
}
|
||||
|
||||
closeConfirmModal()
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const endpointRes = await fetch(
|
||||
const res = await fetch(
|
||||
`${config.routes.api}/${searchSlug}/reindex?locale=${locale.code}`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
@@ -61,9 +60,12 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
},
|
||||
)
|
||||
|
||||
const { message } = (await endpointRes.json()) as { message: string }
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
if (!endpointRes.ok) {
|
||||
const { message } = (await res.json()) as { message: string }
|
||||
|
||||
if (!res.ok) {
|
||||
toast.error(message)
|
||||
} else {
|
||||
toast.success(message)
|
||||
@@ -72,10 +74,13 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
} catch (_err: unknown) {
|
||||
// swallow error, toast shown above
|
||||
} finally {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
setReindexCollections([])
|
||||
setLoading(false)
|
||||
}
|
||||
}, [closeConfirmModal, isLoading, reindexCollections, router, searchSlug, locale, config])
|
||||
},
|
||||
[reindexCollections, router, searchSlug, locale, config],
|
||||
)
|
||||
|
||||
const handleShowConfirmModal = useCallback(
|
||||
(collections: string | string[] = searchCollections) => {
|
||||
@@ -148,14 +153,12 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
|
||||
size="large"
|
||||
verticalAlign="bottom"
|
||||
/>
|
||||
<ReindexConfirmModal
|
||||
description={modalDescription}
|
||||
onCancel={closeConfirmModal}
|
||||
<ConfirmationModal
|
||||
body={modalDescription}
|
||||
heading={modalTitle}
|
||||
modalSlug={confirmReindexModalSlug}
|
||||
onConfirm={handleReindexSubmit}
|
||||
slug={confirmReindexModalSlug}
|
||||
title={modalTitle}
|
||||
/>
|
||||
{isLoading && <LoadingOverlay loadingText={loadingText} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.generate-confirmation {
|
||||
.confirmation-modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
104
packages/ui/src/elements/ConfirmationModal/index.tsx
Normal file
104
packages/ui/src/elements/ConfirmationModal/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import React from 'react'
|
||||
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { drawerZBase } from '../Drawer/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'confirmation-modal'
|
||||
|
||||
export type OnConfirm = (args: {
|
||||
closeConfirmationModal: () => void
|
||||
setConfirming: (state: boolean) => void
|
||||
}) => Promise<void> | void
|
||||
|
||||
export type OnCancel = () => void
|
||||
|
||||
export type ConfirmationModalProps = {
|
||||
body: React.ReactNode
|
||||
cancelLabel?: string
|
||||
confirmingLabel?: string
|
||||
confirmLabel?: string
|
||||
heading: React.ReactNode
|
||||
modalSlug: string
|
||||
onCancel?: OnCancel
|
||||
onConfirm: OnConfirm
|
||||
}
|
||||
|
||||
export function ConfirmationModal(props: ConfirmationModalProps) {
|
||||
const {
|
||||
body,
|
||||
cancelLabel,
|
||||
confirmingLabel,
|
||||
confirmLabel,
|
||||
heading,
|
||||
modalSlug,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
} = props
|
||||
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
const [confirming, setConfirming] = React.useState(false)
|
||||
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={baseClass}
|
||||
slug={modalSlug}
|
||||
style={{
|
||||
zIndex: drawerZBase + editDepth,
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{heading}</h1>
|
||||
<p>{body}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
disabled={confirming}
|
||||
id="confirm-cancel"
|
||||
onClick={
|
||||
confirming
|
||||
? undefined
|
||||
: () => {
|
||||
closeModal(modalSlug)
|
||||
if (typeof onCancel === 'function') {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{cancelLabel || t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
id="confirm-action"
|
||||
onClick={() => {
|
||||
if (!confirming) {
|
||||
setConfirming(true)
|
||||
void onConfirm({
|
||||
closeConfirmationModal: () => closeModal(modalSlug),
|
||||
setConfirming: (state) => setConfirming(state),
|
||||
})
|
||||
}
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{confirming
|
||||
? confirmingLabel || t('general:loading')
|
||||
: confirmLabel || t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -11,34 +11,5 @@
|
||||
&__toggle {
|
||||
@extend %btn-reset;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
max-width: base(36);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
'use client'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
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 } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
|
||||
|
||||
import { useForm } from '../../forms/Form/context.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { drawerZBase } from '../Drawer/index.js'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { PopupList } from '../Popup/index.js'
|
||||
import { Translation } from '../Translation/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'delete-document'
|
||||
|
||||
export type Props = {
|
||||
readonly buttonId?: string
|
||||
readonly collectionSlug: SanitizedCollectionConfig['slug']
|
||||
@@ -58,31 +55,22 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||
|
||||
const { setModified } = useForm()
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const { closeModal, toggleModal } = useModal()
|
||||
const router = useRouter()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { title } = useDocumentInfo()
|
||||
const editDepth = useEditDepth()
|
||||
const { startRouteTransition } = useRouteTransition()
|
||||
const { openModal } = useModal()
|
||||
|
||||
const titleToRender = titleFromProps || title || id
|
||||
|
||||
const modalSlug = `delete-${id}`
|
||||
|
||||
const addDefaultError = useCallback(() => {
|
||||
setDeleting(false)
|
||||
toast.error(t('error:deletingTitle', { title }))
|
||||
}, [t, title])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
closeModal(modalSlug)
|
||||
}
|
||||
}, [closeModal, modalSlug])
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
setDeleting(true)
|
||||
const handleDelete: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
setModified(false)
|
||||
|
||||
try {
|
||||
@@ -96,14 +84,15 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const json = await res.json()
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
if (res.status < 400) {
|
||||
setDeleting(false)
|
||||
toggleModal(modalSlug)
|
||||
|
||||
toast.success(
|
||||
t('general:titleDeleted', { label: getTranslation(singularLabel, i18n), title }) ||
|
||||
json.message,
|
||||
t('general:titleDeleted', {
|
||||
label: getTranslation(singularLabel, i18n),
|
||||
title,
|
||||
}) || json.message,
|
||||
)
|
||||
|
||||
if (redirectAfterDelete) {
|
||||
@@ -121,46 +110,45 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
await onDelete({ id, collectionConfig })
|
||||
}
|
||||
|
||||
toggleModal(modalSlug)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
toggleModal(modalSlug)
|
||||
|
||||
if (json.errors) {
|
||||
json.errors.forEach((error) => toast.error(error.message))
|
||||
} else {
|
||||
addDefaultError()
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (e) {
|
||||
} catch (_err) {
|
||||
return addDefaultError()
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
addDefaultError()
|
||||
} catch (_err) {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
return addDefaultError()
|
||||
}
|
||||
}, [
|
||||
},
|
||||
[
|
||||
setModified,
|
||||
serverURL,
|
||||
api,
|
||||
collectionSlug,
|
||||
id,
|
||||
toggleModal,
|
||||
modalSlug,
|
||||
t,
|
||||
singularLabel,
|
||||
addDefaultError,
|
||||
i18n,
|
||||
title,
|
||||
router,
|
||||
adminRoute,
|
||||
addDefaultError,
|
||||
redirectAfterDelete,
|
||||
onDelete,
|
||||
collectionConfig,
|
||||
startRouteTransition,
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
@@ -168,23 +156,13 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
<PopupList.Button
|
||||
id={buttonId}
|
||||
onClick={() => {
|
||||
setDeleting(false)
|
||||
toggleModal(modalSlug)
|
||||
openModal(modalSlug)
|
||||
}}
|
||||
>
|
||||
{t('general:delete')}
|
||||
</PopupList.Button>
|
||||
<Modal
|
||||
className={baseClass}
|
||||
slug={modalSlug}
|
||||
style={{
|
||||
zIndex: drawerZBase + editDepth,
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:confirmDeletion')}</h1>
|
||||
<p>
|
||||
<ConfirmationModal
|
||||
body={
|
||||
<Translation
|
||||
elements={{
|
||||
'1': ({ children }) => <strong>{children}</strong>,
|
||||
@@ -196,32 +174,12 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
title: titleToRender,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
id="confirm-cancel"
|
||||
onClick={deleting ? undefined : () => toggleModal(modalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
id="confirm-delete"
|
||||
onClick={() => {
|
||||
if (!deleting) {
|
||||
void handleDelete()
|
||||
}
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{deleting ? t('general:deleting') : t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
confirmingLabel={t('general:deleting')}
|
||||
heading={t('general:confirmDeletion')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,34 +7,5 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
max-width: base(36);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
import * as qs from 'qs-esm'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useRouteCache } from '../../providers/RouteCache/index.js'
|
||||
@@ -15,7 +17,7 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j
|
||||
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 { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { Pill } from '../Pill/index.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -36,10 +38,9 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
serverURL,
|
||||
},
|
||||
} = useConfig()
|
||||
const { toggleModal } = useModal()
|
||||
const { openModal } = useModal()
|
||||
const { count, getQueryParams, selectAll, toggleAll } = useSelection()
|
||||
const { i18n, t } = useTranslation()
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
@@ -53,9 +54,8 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
toast.error(t('error:unknown'))
|
||||
}, [t])
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
setDeleting(true)
|
||||
|
||||
const handleDelete: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
const queryWithSearch = mergeListSearchAndWhere({
|
||||
collectionConfig: collection,
|
||||
search: searchParams.get('search'),
|
||||
@@ -73,7 +73,8 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const json = await res.json()
|
||||
toggleModal(modalSlug)
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
const deletedDocs = json?.docs.length || 0
|
||||
const successLabel = deletedDocs > 1 ? plural : singular
|
||||
@@ -113,20 +114,22 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
description: json.errors.map((error) => error.message).join('\n'),
|
||||
})
|
||||
} else {
|
||||
addDefaultError()
|
||||
return addDefaultError()
|
||||
}
|
||||
return false
|
||||
} catch (_err) {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
return addDefaultError()
|
||||
}
|
||||
})
|
||||
}, [
|
||||
},
|
||||
[
|
||||
searchParams,
|
||||
addDefaultError,
|
||||
api,
|
||||
getQueryParams,
|
||||
i18n,
|
||||
modalSlug,
|
||||
plural,
|
||||
router,
|
||||
selectAll,
|
||||
@@ -135,10 +138,10 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
slug,
|
||||
t,
|
||||
toggleAll,
|
||||
toggleModal,
|
||||
clearRouteCache,
|
||||
collection,
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
if (selectAll === SelectAllStatus.None || !hasDeletePermission) {
|
||||
return null
|
||||
@@ -149,39 +152,21 @@ export const DeleteMany: React.FC<Props> = (props) => {
|
||||
<Pill
|
||||
className={`${baseClass}__toggle`}
|
||||
onClick={() => {
|
||||
setDeleting(false)
|
||||
toggleModal(modalSlug)
|
||||
openModal(modalSlug)
|
||||
}}
|
||||
>
|
||||
{t('general:delete')}
|
||||
</Pill>
|
||||
<Modal className={baseClass} slug={modalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:confirmDeletion')}</h1>
|
||||
<p>
|
||||
{t('general:aboutToDeleteCount', {
|
||||
<ConfirmationModal
|
||||
body={t('general:aboutToDeleteCount', {
|
||||
count,
|
||||
label: getTranslation(count > 1 ? plural : singular, i18n),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
id="confirm-cancel"
|
||||
onClick={deleting ? undefined : () => toggleModal(modalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button id="confirm-delete" onClick={deleting ? undefined : handleDelete} size="large">
|
||||
{deleting ? t('general:deleting') : t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
confirmingLabel={t('general:deleting')}
|
||||
heading={t('general:confirmDeletion')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.duplicate {
|
||||
&__modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
max-width: base(36);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,25 @@
|
||||
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
|
||||
|
||||
import { useForm, useFormModified } from '../../forms/Form/context.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { drawerZBase } from '../Drawer/index.js'
|
||||
import './index.scss'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { PopupList } from '../Popup/index.js'
|
||||
|
||||
const baseClass = 'duplicate'
|
||||
|
||||
export type Props = {
|
||||
readonly id: string
|
||||
readonly onDuplicate?: DocumentDrawerContextType['onDuplicate']
|
||||
@@ -42,7 +38,7 @@ export const DuplicateDocument: React.FC<Props> = ({
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const modified = useFormModified()
|
||||
const { toggleModal } = useModal()
|
||||
const { openModal } = useModal()
|
||||
const locale = useLocale()
|
||||
const { setModified } = useForm()
|
||||
const { startRouteTransition } = useRouteTransition()
|
||||
@@ -57,21 +53,15 @@ export const DuplicateDocument: React.FC<Props> = ({
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: slug })
|
||||
|
||||
const [hasClicked, setHasClicked] = useState<boolean>(false)
|
||||
const [renderModal, setRenderModal] = React.useState(false)
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const modalSlug = `duplicate-${id}`
|
||||
|
||||
const editDepth = useEditDepth()
|
||||
const duplicate = useCallback(
|
||||
async ({ onResponse }: { onResponse?: () => void } = {}) => {
|
||||
setRenderModal(true)
|
||||
|
||||
const handleClick = useCallback(
|
||||
async (override = false) => {
|
||||
setHasClicked(true)
|
||||
|
||||
if (modified && !override) {
|
||||
toggleModal(modalSlug)
|
||||
return
|
||||
}
|
||||
await requests
|
||||
.post(
|
||||
`${serverURL}${apiRoute}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
@@ -86,6 +76,10 @@ export const DuplicateDocument: React.FC<Props> = ({
|
||||
)
|
||||
.then(async (res) => {
|
||||
const { doc, errors, message } = await res.json()
|
||||
if (typeof onResponse === 'function') {
|
||||
onResponse()
|
||||
}
|
||||
|
||||
if (res.status < 400) {
|
||||
toast.success(
|
||||
message ||
|
||||
@@ -119,14 +113,11 @@ export const DuplicateDocument: React.FC<Props> = ({
|
||||
},
|
||||
[
|
||||
locale,
|
||||
modified,
|
||||
serverURL,
|
||||
apiRoute,
|
||||
slug,
|
||||
id,
|
||||
i18n,
|
||||
toggleModal,
|
||||
modalSlug,
|
||||
t,
|
||||
singularLabel,
|
||||
onDuplicate,
|
||||
@@ -139,45 +130,43 @@ export const DuplicateDocument: React.FC<Props> = ({
|
||||
],
|
||||
)
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
setHasClicked(false)
|
||||
await handleClick(true)
|
||||
}, [handleClick])
|
||||
const onConfirm: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
setRenderModal(false)
|
||||
|
||||
await duplicate({
|
||||
onResponse: () => {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
},
|
||||
})
|
||||
},
|
||||
[duplicate],
|
||||
)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PopupList.Button id="action-duplicate" onClick={() => void handleClick(false)}>
|
||||
{t('general:duplicate')}
|
||||
</PopupList.Button>
|
||||
{modified && hasClicked && (
|
||||
<Modal
|
||||
className={`${baseClass}__modal`}
|
||||
slug={modalSlug}
|
||||
style={{
|
||||
zIndex: drawerZBase + editDepth,
|
||||
<PopupList.Button
|
||||
id="action-duplicate"
|
||||
onClick={() => {
|
||||
if (modified) {
|
||||
setRenderModal(true)
|
||||
return openModal(modalSlug)
|
||||
}
|
||||
|
||||
return duplicate()
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:confirmDuplication')}</h1>
|
||||
<p>{t('general:unsavedChangesDuplicate')}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
id="confirm-cancel"
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button id="confirm-duplicate" onClick={() => void confirm()} size="large">
|
||||
{t('general:duplicateWithoutSaving')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{t('general:duplicate')}
|
||||
</PopupList.Button>
|
||||
{renderModal && (
|
||||
<ConfirmationModal
|
||||
body={t('general:unsavedChangesDuplicate')}
|
||||
confirmLabel={t('general:duplicateWithoutSaving')}
|
||||
heading={t('general:unsavedChanges')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import React from 'react'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import React, { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { Translation } from '../Translation/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'generate-confirmation'
|
||||
|
||||
export type GenerateConfirmationProps = {
|
||||
highlightField: (Boolean) => void
|
||||
setKey: () => void
|
||||
}
|
||||
|
||||
export const GenerateConfirmation: React.FC<GenerateConfirmationProps> = (props) => {
|
||||
export function GenerateConfirmation(props: GenerateConfirmationProps) {
|
||||
const { highlightField, setKey } = props
|
||||
|
||||
const { id } = useDocumentInfo()
|
||||
@@ -25,12 +25,16 @@ export const GenerateConfirmation: React.FC<GenerateConfirmationProps> = (props)
|
||||
|
||||
const modalSlug = `generate-confirmation-${id}`
|
||||
|
||||
const handleGenerate = () => {
|
||||
const handleGenerate: OnConfirm = useCallback(
|
||||
({ closeConfirmationModal, setConfirming }) => {
|
||||
setKey()
|
||||
toggleModal(modalSlug)
|
||||
toast.success(t('authentication:newAPIKeyGenerated'))
|
||||
highlightField(true)
|
||||
}
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
},
|
||||
[highlightField, setKey, t],
|
||||
)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -43,11 +47,8 @@ export const GenerateConfirmation: React.FC<GenerateConfirmationProps> = (props)
|
||||
>
|
||||
{t('authentication:generateNewAPIKey')}
|
||||
</Button>
|
||||
<Modal className={baseClass} slug={modalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('authentication:confirmGeneration')}</h1>
|
||||
<p>
|
||||
<ConfirmationModal
|
||||
body={
|
||||
<Translation
|
||||
elements={{
|
||||
1: ({ children }) => <strong>{children}</strong>,
|
||||
@@ -55,23 +56,12 @@ export const GenerateConfirmation: React.FC<GenerateConfirmationProps> = (props)
|
||||
i18nKey="authentication:generatingNewAPIKeyWillInvalidate"
|
||||
t={t}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
toggleModal(modalSlug)
|
||||
}}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleGenerate}>{t('authentication:generate')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
confirmLabel={t('authentication:generate')}
|
||||
heading={t('authentication:confirmGeneration')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={handleGenerate}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.leave-without-saving {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,30 @@
|
||||
'use client'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { OnCancel, OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { useForm, useFormModified } from '../../forms/Form/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { Modal, useModal } from '../Modal/index.js'
|
||||
import './index.scss'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { useModal } from '../Modal/index.js'
|
||||
import { usePreventLeave } from './usePreventLeave.js'
|
||||
|
||||
const modalSlug = 'leave-without-saving'
|
||||
|
||||
const baseClass = 'leave-without-saving'
|
||||
|
||||
const Component: React.FC<{
|
||||
isActive: boolean
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
}> = ({ isActive, onCancel, onConfirm }) => {
|
||||
const { closeModal, openModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Manually check for modal state as 'esc' key will not trigger the nav inactivity
|
||||
// useEffect(() => {
|
||||
// if (!modalState?.[modalSlug]?.isOpen && isActive) {
|
||||
// onCancel()
|
||||
// }
|
||||
// }, [modalState, isActive, onCancel])
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
openModal(modalSlug)
|
||||
} else {
|
||||
closeModal(modalSlug)
|
||||
}
|
||||
}, [isActive, openModal, closeModal])
|
||||
|
||||
return (
|
||||
<Modal className={baseClass} onClose={onCancel} slug={modalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('general:leaveWithoutSaving')}</h1>
|
||||
<p>{t('general:changesNotSaved')}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button buttonStyle="secondary" onClick={onCancel} size="large">
|
||||
{t('general:stayOnThisPage')}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} size="large">
|
||||
{t('general:leaveAnyway')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export const LeaveWithoutSaving: React.FC = () => {
|
||||
const { closeModal } = useModal()
|
||||
const { closeModal, openModal } = useModal()
|
||||
const modified = useFormModified()
|
||||
const { isValid } = useForm()
|
||||
const { user } = useAuth()
|
||||
const [show, setShow] = React.useState(false)
|
||||
const [hasAccepted, setHasAccepted] = React.useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const prevent = Boolean((modified || !isValid) && user)
|
||||
|
||||
const onPrevent = useCallback(() => {
|
||||
setShow(true)
|
||||
}, [])
|
||||
openModal(modalSlug)
|
||||
}, [openModal])
|
||||
|
||||
const handleAccept = useCallback(() => {
|
||||
closeModal(modalSlug)
|
||||
@@ -76,15 +32,25 @@ export const LeaveWithoutSaving: React.FC = () => {
|
||||
|
||||
usePreventLeave({ hasAccepted, onAccept: handleAccept, onPrevent, prevent })
|
||||
|
||||
return (
|
||||
<Component
|
||||
isActive={show}
|
||||
onCancel={() => {
|
||||
setShow(false)
|
||||
}}
|
||||
onConfirm={() => {
|
||||
const onCancel: OnCancel = useCallback(() => {
|
||||
closeModal(modalSlug)
|
||||
}, [closeModal])
|
||||
|
||||
const onConfirm: OnConfirm = useCallback(({ closeConfirmationModal, setConfirming }) => {
|
||||
setHasAccepted(true)
|
||||
}}
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
body={t('general:changesNotSaved')}
|
||||
cancelLabel={t('general:stayOnThisPage')}
|
||||
confirmLabel={t('general:leaveAnyway')}
|
||||
heading={t('general:leaveWithoutSaving')}
|
||||
modalSlug={modalSlug}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.publish-many {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
import * as qs from 'qs-esm'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useRouteCache } from '../../providers/RouteCache/index.js'
|
||||
@@ -15,16 +17,15 @@ import { SelectAllStatus, useSelection } from '../../providers/Selection/index.j
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { Pill } from '../Pill/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'publish-many'
|
||||
|
||||
export type PublishManyProps = {
|
||||
collection: ClientCollectionConfig
|
||||
}
|
||||
|
||||
const baseClass = 'publish-many'
|
||||
|
||||
export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
@@ -36,13 +37,13 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
serverURL,
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
const { permissions } = useAuth()
|
||||
const { toggleModal } = useModal()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { getQueryParams, selectAll } = useSelection()
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { openModal } = useModal()
|
||||
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
const hasPermission = collectionPermissions?.update
|
||||
@@ -53,8 +54,8 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
toast.error(t('error:unknown'))
|
||||
}, [t])
|
||||
|
||||
const handlePublish = useCallback(async () => {
|
||||
setSubmitted(true)
|
||||
const handlePublish: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
await requests
|
||||
.patch(
|
||||
`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}&draft=true`,
|
||||
@@ -71,7 +72,8 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const json = await res.json()
|
||||
toggleModal(modalSlug)
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
const deletedDocs = json?.docs.length || 0
|
||||
const successLabel = deletedDocs > 1 ? plural : singular
|
||||
@@ -110,18 +112,19 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
addDefaultError()
|
||||
}
|
||||
return false
|
||||
} catch (e) {
|
||||
} catch (_err) {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
return addDefaultError()
|
||||
}
|
||||
})
|
||||
}, [
|
||||
},
|
||||
[
|
||||
serverURL,
|
||||
api,
|
||||
slug,
|
||||
getQueryParams,
|
||||
i18n,
|
||||
toggleModal,
|
||||
modalSlug,
|
||||
plural,
|
||||
singular,
|
||||
t,
|
||||
@@ -130,7 +133,8 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
selectAll,
|
||||
clearRouteCache,
|
||||
addDefaultError,
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {
|
||||
return null
|
||||
@@ -141,38 +145,20 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
|
||||
<Pill
|
||||
className={`${baseClass}__toggle`}
|
||||
onClick={() => {
|
||||
setSubmitted(false)
|
||||
toggleModal(modalSlug)
|
||||
openModal(modalSlug)
|
||||
}}
|
||||
>
|
||||
{t('version:publish')}
|
||||
</Pill>
|
||||
<Modal className={baseClass} slug={modalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('version:confirmPublish')}</h1>
|
||||
<p>{t('version:aboutToPublishSelection', { label: getTranslation(plural, i18n) })}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
id="confirm-cancel"
|
||||
onClick={submitted ? undefined : () => toggleModal(modalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
id="confirm-publish"
|
||||
onClick={submitted ? undefined : handlePublish}
|
||||
size="large"
|
||||
>
|
||||
{submitted ? t('version:publishing') : t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
body={t('version:aboutToPublishSelection', { label: getTranslation(plural, i18n) })}
|
||||
cancelLabel={t('general:cancel')}
|
||||
confirmingLabel={t('version:publishing')}
|
||||
confirmLabel={t('general:confirm')}
|
||||
heading={t('version:confirmPublish')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={handlePublish}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,46 +17,5 @@
|
||||
&__action {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__modal {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__toggle {
|
||||
@extend %btn-reset;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
max-width: base(36);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import React, { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { useForm } from '../../forms/Form/context.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useEditDepth } from '../../providers/EditDepth/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { drawerZBase } from '../Drawer/index.js'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'status'
|
||||
@@ -36,13 +37,10 @@ export const Status: React.FC = () => {
|
||||
serverURL,
|
||||
},
|
||||
} = useConfig()
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const { reset: resetForm } = useForm()
|
||||
const { code: locale } = useLocale()
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const editDepth = useEditDepth()
|
||||
|
||||
const unPublishModalSlug = `confirm-un-publish-${id}`
|
||||
const revertModalSlug = `confirm-revert-${id}`
|
||||
|
||||
@@ -57,13 +55,14 @@ export const Status: React.FC = () => {
|
||||
}
|
||||
|
||||
const performAction = useCallback(
|
||||
async (action: 'revert' | 'unpublish') => {
|
||||
async (
|
||||
action: 'revert' | 'unpublish',
|
||||
{ closeConfirmationModal, setConfirming }: Parameters<OnConfirm>[0],
|
||||
) => {
|
||||
let url
|
||||
let method
|
||||
let body
|
||||
|
||||
setProcessing(true)
|
||||
|
||||
if (action === 'unpublish') {
|
||||
body = {
|
||||
_status: 'draft',
|
||||
@@ -74,6 +73,7 @@ export const Status: React.FC = () => {
|
||||
url = `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}&fallback-locale=null&depth=0`
|
||||
method = 'patch'
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
url = `${serverURL}${api}/globals/${globalSlug}?locale=${locale}&fallback-locale=null&depth=0`
|
||||
method = 'post'
|
||||
@@ -100,6 +100,9 @@ export const Status: React.FC = () => {
|
||||
},
|
||||
})
|
||||
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
if (res.status === 200) {
|
||||
let data
|
||||
const json = await res.json()
|
||||
@@ -124,15 +127,6 @@ export const Status: React.FC = () => {
|
||||
} else {
|
||||
toast.error(t('error:unPublishingDocument'))
|
||||
}
|
||||
|
||||
setProcessing(false)
|
||||
if (action === 'revert') {
|
||||
toggleModal(revertModalSlug)
|
||||
}
|
||||
|
||||
if (action === 'unpublish') {
|
||||
toggleModal(unPublishModalSlug)
|
||||
}
|
||||
},
|
||||
[
|
||||
api,
|
||||
@@ -145,10 +139,8 @@ export const Status: React.FC = () => {
|
||||
resetForm,
|
||||
serverURL,
|
||||
setUnpublishedVersionCount,
|
||||
setMostRecentVersionIsAutosaved,
|
||||
t,
|
||||
toggleModal,
|
||||
revertModalSlug,
|
||||
unPublishModalSlug,
|
||||
setHasPublishedDoc,
|
||||
],
|
||||
)
|
||||
@@ -174,34 +166,13 @@ export const Status: React.FC = () => {
|
||||
>
|
||||
{t('version:unpublish')}
|
||||
</Button>
|
||||
<Modal
|
||||
className={`${baseClass}__modal`}
|
||||
slug={unPublishModalSlug}
|
||||
style={{ zIndex: drawerZBase + editDepth }}
|
||||
>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('version:confirmUnpublish')}</h1>
|
||||
<p>{t('version:aboutToUnpublish')}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={processing ? undefined : () => toggleModal(unPublishModalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={processing ? undefined : () => performAction('unpublish')}
|
||||
size="large"
|
||||
>
|
||||
{t(processing ? 'version:unpublishing' : 'general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
body={t('version:aboutToUnpublish')}
|
||||
confirmingLabel={t('version:unpublishing')}
|
||||
heading={t('version:confirmUnpublish')}
|
||||
modalSlug={unPublishModalSlug}
|
||||
onConfirm={(args) => performAction('unpublish', args)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{canUpdate && statusToRender === 'changed' && (
|
||||
@@ -215,31 +186,13 @@ export const Status: React.FC = () => {
|
||||
>
|
||||
{t('version:revertToPublished')}
|
||||
</Button>
|
||||
<Modal className={`${baseClass}__modal`} slug={revertModalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('version:confirmRevertToSaved')}</h1>
|
||||
<p>{t('version:aboutToRevertToPublished')}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={processing ? undefined : () => toggleModal(revertModalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
id="action-revert-to-published-confirm"
|
||||
onClick={processing ? undefined : () => performAction('revert')}
|
||||
size="large"
|
||||
>
|
||||
{t(processing ? 'version:reverting' : 'general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
body={t('version:aboutToRevertToPublished')}
|
||||
confirmingLabel={t('version:reverting')}
|
||||
heading={t('version:confirmRevertToSaved')}
|
||||
modalSlug={revertModalSlug}
|
||||
onConfirm={(args) => performAction('revert', args)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.stay-logged-in {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--base);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--base);
|
||||
max-width: base(36);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: var(--base);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import type { OnCancel, OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { Button } from '../../elements/Button/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'stay-logged-in'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
|
||||
export const stayLoggedInModalSlug = 'stay-logged-in'
|
||||
|
||||
@@ -28,22 +26,13 @@ export const StayLoggedInModal: React.FC = () => {
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
const { startRouteTransition } = useRouteTransition()
|
||||
|
||||
return (
|
||||
<Modal className={baseClass} slug={stayLoggedInModalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('authentication:stayLoggedIn')}</h1>
|
||||
<p>{t('authentication:youAreInactive')}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
closeModal(stayLoggedInModalSlug)
|
||||
const onConfirm: OnConfirm = useCallback(
|
||||
({ closeConfirmationModal, setConfirming }) => {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
startRouteTransition(() =>
|
||||
router.push(
|
||||
@@ -53,22 +42,23 @@ export const StayLoggedInModal: React.FC = () => {
|
||||
}),
|
||||
),
|
||||
)
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{t('authentication:logOut')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
},
|
||||
[router, startRouteTransition, adminRoute, logoutRoute],
|
||||
)
|
||||
|
||||
const onCancel: OnCancel = useCallback(() => {
|
||||
refreshCookie()
|
||||
closeModal(stayLoggedInModalSlug)
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
{t('authentication:stayLoggedIn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
}, [refreshCookie])
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
body={t('authentication:youAreInactive')}
|
||||
cancelLabel={t('authentication:stayLoggedIn')}
|
||||
confirmLabel={t('authentication:logOut')}
|
||||
heading={t('authentication:stayLoggedIn')}
|
||||
modalSlug={stayLoggedInModalSlug}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
@import '../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.unpublish-many {
|
||||
@include blur-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&__wrapper {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.8);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(0.4);
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
gap: base(0.4);
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
'use client'
|
||||
import { Modal, useModal } from '@faceless-ui/modal'
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
import * as qs from 'qs-esm'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { OnConfirm } from '../ConfirmationModal/index.js'
|
||||
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
@@ -11,22 +16,16 @@ import { useRouteCache } from '../../providers/RouteCache/index.js'
|
||||
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { Pill } from '../Pill/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'unpublish-many'
|
||||
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
|
||||
import { ConfirmationModal } from '../ConfirmationModal/index.js'
|
||||
import { Pill } from '../Pill/index.js'
|
||||
|
||||
export type UnpublishManyProps = {
|
||||
collection: ClientCollectionConfig
|
||||
}
|
||||
|
||||
const baseClass = 'unpublish-many'
|
||||
|
||||
export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
const { collection: { slug, labels: { plural, singular }, versions } = {} } = props
|
||||
|
||||
@@ -42,7 +41,6 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const searchParams = useSearchParams()
|
||||
const { getQueryParams, selectAll } = useSelection()
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const router = useRouter()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
@@ -55,10 +53,12 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
toast.error(t('error:unknown'))
|
||||
}, [t])
|
||||
|
||||
const handleUnpublish = useCallback(async () => {
|
||||
setSubmitted(true)
|
||||
const handleUnpublish: OnConfirm = useCallback(
|
||||
async ({ closeConfirmationModal, setConfirming }) => {
|
||||
await requests
|
||||
.patch(`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`, {
|
||||
.patch(
|
||||
`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
_status: 'draft',
|
||||
}),
|
||||
@@ -66,11 +66,13 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
},
|
||||
)
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const json = await res.json()
|
||||
toggleModal(modalSlug)
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
|
||||
const deletedDocs = json?.docs.length || 0
|
||||
const successLabel = deletedDocs > 1 ? plural : singular
|
||||
@@ -110,17 +112,18 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
}
|
||||
return false
|
||||
} catch (_err) {
|
||||
setConfirming(false)
|
||||
closeConfirmationModal()
|
||||
return addDefaultError()
|
||||
}
|
||||
})
|
||||
}, [
|
||||
},
|
||||
[
|
||||
serverURL,
|
||||
api,
|
||||
slug,
|
||||
getQueryParams,
|
||||
i18n,
|
||||
toggleModal,
|
||||
modalSlug,
|
||||
plural,
|
||||
singular,
|
||||
t,
|
||||
@@ -129,7 +132,8 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
selectAll,
|
||||
clearRouteCache,
|
||||
addDefaultError,
|
||||
])
|
||||
],
|
||||
)
|
||||
|
||||
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {
|
||||
return null
|
||||
@@ -140,38 +144,18 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
|
||||
<Pill
|
||||
className={`${baseClass}__toggle`}
|
||||
onClick={() => {
|
||||
setSubmitted(false)
|
||||
toggleModal(modalSlug)
|
||||
}}
|
||||
>
|
||||
{t('version:unpublish')}
|
||||
</Pill>
|
||||
<Modal className={baseClass} slug={modalSlug}>
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<h1>{t('version:confirmUnpublish')}</h1>
|
||||
<p>{t('version:aboutToUnpublishSelection', { label: getTranslation(plural, i18n) })}</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
id="confirm-cancel"
|
||||
onClick={submitted ? undefined : () => toggleModal(modalSlug)}
|
||||
size="large"
|
||||
type="button"
|
||||
>
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
id="confirm-unpublish"
|
||||
onClick={submitted ? undefined : handleUnpublish}
|
||||
size="large"
|
||||
>
|
||||
{submitted ? t('version:unpublishing') : t('general:confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
body={t('version:aboutToUnpublishSelection', { label: getTranslation(plural, i18n) })}
|
||||
confirmingLabel={t('version:unpublishing')}
|
||||
heading={t('version:confirmUnpublish')}
|
||||
modalSlug={modalSlug}
|
||||
onConfirm={handleUnpublish}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ export { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
||||
export { useUseTitleField } from '../../hooks/useUseAsTitle.js'
|
||||
|
||||
// elements
|
||||
export { ConfirmationModal } from '../../elements/ConfirmationModal/index.js'
|
||||
export type { OnCancel, OnConfirm } from '../../elements/ConfirmationModal/index.js'
|
||||
export { Link } from '../../elements/Link/index.js'
|
||||
export { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
|
||||
export { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
@@ -73,7 +73,6 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
readOnly || (apiKeyPermissions !== true && !apiKeyPermissions?.update)
|
||||
|
||||
const canReadApiKey = apiKeyPermissions === true || apiKeyPermissions?.read
|
||||
const canReadEnableAPIKey = apiKeyPermissions === true || apiKeyPermissions?.read
|
||||
|
||||
const handleChangePassword = useCallback(
|
||||
(showPasswordFields: boolean) => {
|
||||
@@ -202,7 +201,8 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{useAPIKey && (
|
||||
<div className={`${baseClass}__api-key`}>
|
||||
{canReadEnableAPIKey && (
|
||||
{canReadApiKey && (
|
||||
<Fragment>
|
||||
<CheckboxField
|
||||
field={{
|
||||
name: 'enableAPIKey',
|
||||
@@ -212,8 +212,9 @@ export const Auth: React.FC<Props> = (props) => {
|
||||
path="enableAPIKey"
|
||||
schemaPath={`${collectionSlug}.enableAPIKey`}
|
||||
/>
|
||||
<APIKey enabled={!!enableAPIKey?.value} readOnly={apiKeyReadOnly} />
|
||||
</Fragment>
|
||||
)}
|
||||
{canReadApiKey && <APIKey enabled={!!enableAPIKey?.value} readOnly={apiKeyReadOnly} />}
|
||||
</div>
|
||||
)}
|
||||
{verify && isEditing && (
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
uploads: Upload;
|
||||
posts: Post;
|
||||
|
||||
@@ -533,7 +533,7 @@ describe('relationship', () => {
|
||||
await drawer1Content.locator('#action-delete').click()
|
||||
|
||||
await page
|
||||
.locator('[id^=delete-].payload__modal-item.delete-document[open] button#confirm-delete')
|
||||
.locator('[id^=delete-].payload__modal-item.confirmation-modal[open] button#confirm-action')
|
||||
.click()
|
||||
|
||||
await expect(drawer1Content).toBeHidden()
|
||||
|
||||
@@ -357,7 +357,7 @@ describe('Join Field', () => {
|
||||
await deleteButton.click()
|
||||
const deleteConfirmModal = page.locator('dialog[id^="delete-"][open]')
|
||||
await expect(deleteConfirmModal).toBeVisible()
|
||||
const confirmDeleteButton = deleteConfirmModal.locator('button#confirm-delete')
|
||||
const confirmDeleteButton = deleteConfirmModal.locator('button#confirm-action')
|
||||
await expect(confirmDeleteButton).toBeVisible()
|
||||
await confirmDeleteButton.click()
|
||||
await expect(drawer).toBeHidden()
|
||||
|
||||
@@ -223,7 +223,7 @@ describe('Locked Documents', () => {
|
||||
await page.goto(postsUrl.list)
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await expect(page.locator('.delete-documents__content p')).toHaveText(
|
||||
await expect(page.locator('#delete-posts .confirmation-modal__content p')).toHaveText(
|
||||
'You are about to delete 2 Posts',
|
||||
)
|
||||
})
|
||||
@@ -243,7 +243,7 @@ describe('Locked Documents', () => {
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.list-selection .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('.cell-_select')).toHaveCount(1)
|
||||
})
|
||||
|
||||
@@ -257,12 +257,11 @@ describe('Locked Documents', () => {
|
||||
await page.reload()
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
await page.waitForURL(new RegExp(postsUrl.list))
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.list-selection .list-selection__button').click()
|
||||
await page.locator('.publish-many__toggle').click()
|
||||
await page.locator('#confirm-publish').click()
|
||||
await page.locator('#publish-posts #confirm-action').click()
|
||||
|
||||
const paginator = page.locator('.paginator')
|
||||
|
||||
@@ -273,12 +272,11 @@ describe('Locked Documents', () => {
|
||||
|
||||
test('should only allow bulk unpublish on unlocked documents on all pages', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
await page.waitForURL(new RegExp(postsUrl.list))
|
||||
|
||||
await page.locator('input#select-all').check()
|
||||
await page.locator('.list-selection .list-selection__button').click()
|
||||
await page.locator('.unpublish-many__toggle').click()
|
||||
await page.locator('#confirm-unpublish').click()
|
||||
await page.locator('#unpublish-posts #confirm-action').click()
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
|
||||
'Updated 10 Posts successfully.',
|
||||
)
|
||||
@@ -568,7 +566,9 @@ describe('Locked Documents', () => {
|
||||
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()
|
||||
|
||||
// eslint-disable-next-line payload/no-wait-function
|
||||
await wait(500)
|
||||
@@ -620,7 +620,9 @@ describe('Locked Documents', () => {
|
||||
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()
|
||||
|
||||
// eslint-disable-next-line payload/no-wait-function
|
||||
await wait(500)
|
||||
|
||||
1
test/versions/.gitignore
vendored
Normal file
1
test/versions/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
uploads
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
@@ -134,7 +134,7 @@ describe('Versions', () => {
|
||||
|
||||
await rowToDelete.locator('.cell-_select input').check()
|
||||
await page.locator('.delete-documents__toggle').click()
|
||||
await page.locator('#confirm-delete').click()
|
||||
await page.locator('#delete-draft-posts #confirm-action').click()
|
||||
|
||||
await expect(page.locator('.payload-toast-container .toast-success')).toContainText(
|
||||
'Deleted 1 Draft Post successfully.',
|
||||
@@ -152,7 +152,7 @@ describe('Versions', () => {
|
||||
|
||||
// Bulk edit the selected rows
|
||||
await page.locator('.publish-many__toggle').click()
|
||||
await page.locator('#confirm-publish').click()
|
||||
await page.locator('#publish-draft-posts #confirm-action').click()
|
||||
|
||||
// Check that the statuses for each row has been updated to `published`
|
||||
await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Published')
|
||||
@@ -176,7 +176,7 @@ describe('Versions', () => {
|
||||
await expect(findTableCell(page, '_status', title)).toContainText('Draft')
|
||||
await selectTableRow(page, title)
|
||||
await page.locator('.publish-many__toggle').click()
|
||||
await page.locator('#confirm-publish').click()
|
||||
await page.locator('#publish-autosave-posts #confirm-action').click()
|
||||
await expect(findTableCell(page, '_status', title)).toContainText('Published')
|
||||
})
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('Versions', () => {
|
||||
|
||||
// Bulk edit the selected rows
|
||||
await page.locator('.unpublish-many__toggle').click()
|
||||
await page.locator('#confirm-unpublish').click()
|
||||
await page.locator('#unpublish-draft-posts #confirm-action').click()
|
||||
|
||||
// Check that the statuses for each row has been updated to `draft`
|
||||
await expect(findTableCell(page, '_status', 'Published Title')).toContainText('Draft')
|
||||
@@ -565,7 +565,7 @@ describe('Versions', () => {
|
||||
|
||||
// revert to last published version
|
||||
await page.locator('#action-revert-to-published').click()
|
||||
await saveDocAndAssert(page, '#action-revert-to-published-confirm')
|
||||
await saveDocAndAssert(page, '[id^=confirm-revert-] #confirm-action')
|
||||
|
||||
// verify that spanish content is reverted correctly
|
||||
await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/fields/config.ts"],
|
||||
"@payload-config": ["./test/plugin-search/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user