Files
payloadcms/packages/ui/src/elements/UnpublishMany/index.tsx
Paul 26ffbca914 feat: sanitise access endpoint (#7335)
Protects the `/api/access` endpoint behind authentication and sanitizes
the result, making it more secure and significantly smaller. To do this:

1. The `permission` keyword is completely omitted from the result
2. Only _truthy_ access results are returned
3. All nested permissions are consolidated when possible

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
2024-11-15 15:08:06 -05:00

171 lines
5.1 KiB
TypeScript

'use client'
import { Modal, useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React, { useCallback, useState } from 'react'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useSearchParams } from '../../providers/SearchParams/index.js'
import { SelectAllStatus, useSelection } from '../../providers/Selection/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js'
import { Button } from '../Button/index.js'
import { Pill } from '../Pill/index.js'
import './index.scss'
const baseClass = 'unpublish-many'
import type { ClientCollectionConfig } from 'payload'
import { toast } from 'sonner'
export type UnpublishManyProps = {
collection: ClientCollectionConfig
}
export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
const { collection: { slug, labels: { plural, singular }, versions } = {} } = props
const {
config: {
routes: { api },
serverURL,
},
} = useConfig()
const { permissions } = useAuth()
const { toggleModal } = useModal()
const { i18n, t } = useTranslation()
const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false)
const { stringifyParams } = useSearchParams()
const router = useRouter()
const { clearRouteCache } = useRouteCache()
const collectionPermissions = permissions?.collections?.[slug]
const hasPermission = collectionPermissions?.update
const modalSlug = `unpublish-${slug}`
const addDefaultError = useCallback(() => {
toast.error(t('error:unknown'))
}, [t])
const handleUnpublish = useCallback(async () => {
setSubmitted(true)
await requests
.patch(`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`, {
body: JSON.stringify({
_status: 'draft',
}),
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 successLabel = deletedDocs > 1 ? plural : singular
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
router.replace(
stringifyParams({
params: {
page: selectAll ? '1' : undefined,
},
}),
)
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 {
addDefaultError()
}
return false
} catch (e) {
return addDefaultError()
}
})
}, [
addDefaultError,
api,
getQueryParams,
i18n,
modalSlug,
plural,
selectAll,
serverURL,
singular,
slug,
t,
toggleModal,
router,
clearRouteCache,
stringifyParams,
])
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {
return null
}
return (
<React.Fragment>
<Pill
className={`${baseClass}__toggle`}
onClick={() => {
setSubmitted(false)
toggleModal(modalSlug)
}}
>
{t('version:unpublish')}
</Pill>
<Modal className={baseClass} slug={modalSlug}>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__content`}>
<h1>{t('version:confirmUnpublish')}</h1>
<p>{t('version:aboutToUnpublishSelection', { label: getTranslation(plural, i18n) })}</p>
</div>
<div className={`${baseClass}__controls`}>
<Button
buttonStyle="secondary"
id="confirm-cancel"
onClick={submitted ? undefined : () => toggleModal(modalSlug)}
size="large"
type="button"
>
{t('general:cancel')}
</Button>
<Button
id="confirm-unpublish"
onClick={submitted ? undefined : handleUnpublish}
size="large"
>
{submitted ? t('version:unpublishing') : t('general:confirm')}
</Button>
</div>
</div>
</Modal>
</React.Fragment>
)
}