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