fix(next): establishes pattern for preview urls (#5581)

This commit is contained in:
Jacob Fletcher
2024-04-01 17:30:49 -04:00
committed by GitHub
parent 037ed3cd54
commit 799370f753
15 changed files with 234 additions and 130 deletions

View File

@@ -0,0 +1,45 @@
import httpStatus from 'http-status'
import { findByIDOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
import { routeError } from '../routeError.js'
export const preview: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const result = await findByIDOperation({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
req,
})
let previewURL: string
const generatePreviewURL = req.payload.config.collections.find(
(config) => config.slug === collection.config.slug,
)?.admin?.preview
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
})
} catch (err) {
routeError({
collection,
err,
req,
})
}
}
return Response.json(previewURL, {
status: httpStatus.OK,
})
}

View File

@@ -0,0 +1,44 @@
import httpStatus from 'http-status'
import { findOneOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { GlobalRouteHandler } from '../types.js'
import { routeError } from '../routeError.js'
export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const result = await findOneOperation({
slug: globalConfig.slug,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: searchParams.get('draft') === 'true',
globalConfig,
req,
})
let previewURL: string
const generatePreviewURL = req.payload.config.globals.find(
(config) => config.slug === globalConfig.slug,
)?.admin?.preview
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
})
} catch (err) {
routeError({
err,
req,
})
}
}
return Response.json(previewURL, {
status: httpStatus.OK,
})
}

View File

@@ -34,6 +34,7 @@ import { find } from './collections/find.js'
import { findByID } from './collections/findByID.js'
import { findVersionByID } from './collections/findVersionByID.js'
import { findVersions } from './collections/findVersions.js'
import { preview as previewCollection } from './collections/preview.js'
import { restoreVersion } from './collections/restoreVersion.js'
import { update } from './collections/update.js'
import { updateByID } from './collections/updateByID.js'
@@ -42,6 +43,7 @@ import { docAccess as docAccessGlobal } from './globals/docAccess.js'
import { findOne } from './globals/findOne.js'
import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionByID.js'
import { findVersions as findVersionsGlobal } from './globals/findVersions.js'
import { preview as previewGlobal } from './globals/preview.js'
import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js'
import { update as updateGlobal } from './globals/update.js'
import { routeError } from './routeError.js'
@@ -60,6 +62,7 @@ const endpoints = {
getFile,
init,
me,
preview: previewCollection,
versions: findVersions,
},
PATCH: {
@@ -88,6 +91,7 @@ const endpoints = {
'doc-versions': findVersionsGlobal,
'doc-versions-by-id': findVersionByIdGlobal,
findOne,
preview: previewGlobal,
},
POST: {
'doc-access': docAccessGlobal,
@@ -171,6 +175,7 @@ export const GET =
endpoints: req.payload.config.endpoints,
request,
})
if (disableEndpoints) return disableEndpoints
collection = req.payload.collections?.[slug1]
@@ -212,10 +217,16 @@ export const GET =
if (slug2 === 'file') {
// /:collection/file/:filename
res = await endpoints.collection.GET.getFile({ collection, filename: slug3, req })
} else if (slug3 in endpoints.collection.GET) {
// /:collection/:id/preview
res = await (endpoints.collection.GET[slug3] as CollectionRouteHandlerWithID)({
id: slug2,
collection,
req,
})
} else if (`doc-${slug2}-by-id` in endpoints.collection.GET) {
// /:collection/access/:id
// /:collection/versions/:id
res = await (
endpoints.collection.GET[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req })
@@ -229,6 +240,7 @@ export const GET =
endpoints: globalConfig.endpoints,
request,
})
if (disableEndpoints) return disableEndpoints
const customEndpointResponse = await handleCustomEndpoints({
@@ -236,6 +248,7 @@ export const GET =
entitySlug: `${slug1}/${slug2}`,
payloadRequest: req,
})
if (customEndpointResponse) return customEndpointResponse
switch (slug.length) {
@@ -244,9 +257,16 @@ export const GET =
res = await endpoints.global.GET.findOne({ globalConfig, req })
break
case 3:
if (`doc-${slug3}` in endpoints.global.GET) {
if (slug3 in endpoints.global.GET) {
// /globals/:slug/preview
res = await (endpoints.global.GET[slug3] as GlobalRouteHandler)({
globalConfig,
req,
})
} else if (`doc-${slug3}` in endpoints.global.GET) {
// /globals/:slug/access
// /globals/:slug/versions
// /globals/:slug/preview
res = await (endpoints.global.GET?.[`doc-${slug3}`] as GlobalRouteHandler)({
globalConfig,
req,

View File

@@ -1,11 +1 @@
export type CustomPreviewButtonProps = React.ComponentType<
DefaultPreviewButtonProps & {
DefaultButton: React.ComponentType<DefaultPreviewButtonProps>
}
>
export type DefaultPreviewButtonProps = {
disabled: boolean
label: string
preview: () => void
}
export type CustomPreviewButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomPublishButtonProps = React.ComponentType
export type CustomPublishButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomSaveButtonProps = React.ComponentType
export type CustomSaveButton = React.ComponentType

View File

@@ -1 +1 @@
export type CustomSaveDraftButtonProps = React.ComponentType
export type CustomSaveDraftButton = React.ComponentType

View File

@@ -2,11 +2,10 @@ export type { RichTextAdapter, RichTextFieldProps } from './RichText.js'
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
export type { ConditionalDateProps } from './elements/DatePicker.js'
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
export type { DefaultPreviewButtonProps } from './elements/PreviewButton.js'
export type { CustomPreviewButtonProps } from './elements/PreviewButton.js'
export type { CustomPublishButtonProps } from './elements/PublishButton.js'
export type { CustomSaveButtonProps } from './elements/SaveButton.js'
export type { CustomSaveDraftButtonProps } from './elements/SaveDraftButton.js'
export type { CustomPreviewButton } from './elements/PreviewButton.js'
export type { CustomPublishButton } from './elements/PublishButton.js'
export type { CustomSaveButton } from './elements/SaveButton.js'
export type { CustomSaveDraftButton } from './elements/SaveDraftButton.js'
export type {
DocumentTab,
DocumentTabComponent,

View File

@@ -2,10 +2,10 @@ import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from '
import type { DeepRequired } from 'ts-essentials'
import type {
CustomPreviewButtonProps,
CustomPublishButtonProps,
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
CustomPreviewButton,
CustomPublishButton,
CustomSaveButton,
CustomSaveDraftButton,
} from '../../admin/types.js'
import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js'
import type {
@@ -211,23 +211,23 @@ export type CollectionAdminOptions = {
/**
* Replaces the "Preview" button
*/
PreviewButton?: CustomPreviewButtonProps
PreviewButton?: CustomPreviewButton
/**
* Replaces the "Publish" button
* + drafts must be enabled
*/
PublishButton?: CustomPublishButtonProps
PublishButton?: CustomPublishButton
/**
* Replaces the "Save" button
* + drafts must be disabled
*/
SaveButton?: CustomSaveButtonProps
SaveButton?: CustomSaveButton
/**
* Replaces the "Save Draft" button
* + drafts must be enabled
* + autosave must be disabled
*/
SaveDraftButton?: CustomSaveDraftButtonProps
SaveDraftButton?: CustomSaveDraftButton
}
views?: {
/**

View File

@@ -2,10 +2,10 @@ import type { GraphQLNonNull, GraphQLObjectType } from 'graphql'
import type { DeepRequired } from 'ts-essentials'
import type {
CustomPreviewButtonProps,
CustomPublishButtonProps,
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
CustomPreviewButton,
CustomPublishButton,
CustomSaveButton,
CustomSaveDraftButton,
} from '../../admin/types.js'
import type { User } from '../../auth/types.js'
import type {
@@ -79,23 +79,23 @@ export type GlobalAdminOptions = {
/**
* Replaces the "Preview" button
*/
PreviewButton?: CustomPreviewButtonProps
PreviewButton?: CustomPreviewButton
/**
* Replaces the "Publish" button
* + drafts must be enabled
*/
PublishButton?: CustomPublishButtonProps
PublishButton?: CustomPublishButton
/**
* Replaces the "Save" button
* + drafts must be disabled
*/
SaveButton?: CustomSaveButtonProps
SaveButton?: CustomSaveButton
/**
* Replaces the "Save Draft" button
* + drafts must be enabled
* + autosave must be disabled
*/
SaveDraftButton?: CustomSaveDraftButtonProps
SaveDraftButton?: CustomSaveDraftButton
}
views?: {
/**

View File

@@ -154,15 +154,9 @@ export const DocumentControls: React.FC<{
</div>
<div className={`${baseClass}__controls-wrapper`}>
<div className={`${baseClass}__controls`}>
{/* {(collectionConfig?.admin?.preview || globalConfig?.admin?.preview) && (
<PreviewButton
CustomComponent={
collectionConfig?.admin?.components?.edit?.PreviewButton ||
globalConfig?.admin?.components?.elements?.PreviewButton
}
generatePreviewURL={collectionConfig?.admin?.preview || globalConfig?.admin?.preview}
/>
)} */}
{componentMap?.isPreviewEnabled && (
<PreviewButton CustomComponent={componentMap.PreviewButton} />
)}
{hasSavePermission && (
<React.Fragment>
{collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts ? (

View File

@@ -1,31 +1,24 @@
'use client'
import type { GeneratePreviewURL } from 'payload/config'
import type { CustomPreviewButtonProps, DefaultPreviewButtonProps } from 'payload/types'
import React from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Button } from '../Button/index.js'
import { usePreviewURL } from './usePreviewURL.js'
const baseClass = 'preview-btn'
const DefaultPreviewButton: React.FC<DefaultPreviewButtonProps> = ({
disabled,
label,
preview,
}) => {
const DefaultPreviewButton: React.FC = () => {
const { generatePreviewURL, label } = usePreviewURL()
return (
<Button
buttonStyle="secondary"
className={baseClass}
disabled={disabled}
onClick={preview}
// disabled={disabled}
onClick={() =>
generatePreviewURL({
openPreviewWindow: true,
})
}
size="small"
>
{label}
@@ -33,66 +26,12 @@ const DefaultPreviewButton: React.FC<DefaultPreviewButtonProps> = ({
)
}
export type PreviewButtonProps = {
CustomComponent?: CustomPreviewButtonProps
generatePreviewURL?: GeneratePreviewURL
type Props = {
CustomComponent?: React.ReactNode
}
export const PreviewButton: React.FC<PreviewButtonProps> = ({
CustomComponent,
generatePreviewURL,
}) => {
const { id, collectionSlug, globalSlug } = useDocumentInfo()
export const PreviewButton: React.FC<Props> = ({ CustomComponent }) => {
if (CustomComponent) return CustomComponent
const [isLoading, setIsLoading] = useState(false)
const { code: locale } = useLocale()
const { token } = useAuth()
const {
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation()
const isGeneratingPreviewURL = useRef(false)
// we need to regenerate the preview URL every time the button is clicked
// to do this we need to fetch the document data fresh from the API
// this will ensure the latest data is used when generating the preview URL
const preview = useCallback(async () => {
if (!generatePreviewURL || isGeneratingPreviewURL.current) return
isGeneratingPreviewURL.current = true
try {
setIsLoading(true)
let url = `${serverURL}${api}`
if (collectionSlug) url = `${url}/${collectionSlug}/${id}`
if (globalSlug) url = `${url}/globals/${globalSlug}`
const data = await fetch(`${url}?draft=true&locale=${locale}&fallback-locale=null`).then(
(res) => res.json(),
)
const previewURL = await generatePreviewURL(data, { locale, token })
if (!previewURL) throw new Error()
setIsLoading(false)
isGeneratingPreviewURL.current = false
window.open(previewURL, '_blank')
} catch (err) {
setIsLoading(false)
isGeneratingPreviewURL.current = false
toast.error(t('error:previewing'))
}
}, [serverURL, api, collectionSlug, globalSlug, id, generatePreviewURL, locale, token, t])
return (
<RenderCustomComponent
CustomComponent={CustomComponent}
DefaultComponent={DefaultPreviewButton}
componentProps={{
DefaultButton: DefaultPreviewButton,
disabled: isLoading || !generatePreviewURL,
label: isLoading ? t('general:loading') : t('version:preview'),
preview,
}}
/>
)
return <DefaultPreviewButton />
}

View File

@@ -0,0 +1,70 @@
'use client'
import { useCallback, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
export const usePreviewURL = (): {
generatePreviewURL: ({ openPreviewWindow }: { openPreviewWindow?: boolean }) => void
isLoading: boolean
label: string
previewURL: string
} => {
const { id, collectionSlug, globalSlug } = useDocumentInfo()
const [isLoading, setIsLoading] = useState(false)
const [previewURL, setPreviewURL] = useState('')
const { code: locale } = useLocale()
const {
routes: { api },
serverURL,
} = useConfig()
const { t } = useTranslation()
const isGeneratingPreviewURL = useRef(false)
// we need to regenerate the preview URL every time the button is clicked
// to do this we need to fetch the document data fresh from the API
// this will ensure the latest data is used when generating the preview URL
const generatePreviewURL = useCallback(
async ({ openPreviewWindow = false }) => {
if (isGeneratingPreviewURL.current) return
isGeneratingPreviewURL.current = true
try {
setIsLoading(true)
let url = `${serverURL}${api}`
if (collectionSlug) url = `${url}/${collectionSlug}/${id}/preview`
if (globalSlug) url = `${url}/globals/${globalSlug}/preview`
const res = await fetch(`${url}${locale ? `?locale=${locale}` : ''}`)
if (!res.ok) throw new Error()
const newPreviewURL = await res.json()
if (!newPreviewURL) throw new Error()
setPreviewURL(newPreviewURL)
setIsLoading(false)
isGeneratingPreviewURL.current = false
if (openPreviewWindow) window.open(newPreviewURL, '_blank')
} catch (err) {
setIsLoading(false)
isGeneratingPreviewURL.current = false
toast.error(t('error:previewing'))
}
},
[serverURL, api, collectionSlug, globalSlug, id, locale, t],
)
return {
generatePreviewURL,
isLoading,
label: isLoading ? t('general:loading') : t('version:preview'),
previewURL,
}
}

View File

@@ -70,8 +70,8 @@ export const buildComponentMap = (args: {
const SaveDraftButtonComponent = collectionConfig?.admin?.components?.edit?.SaveDraftButton
const SaveDraftButton = SaveDraftButtonComponent ? <SaveDraftButtonComponent /> : undefined
/* const PreviewButtonComponent = collectionConfig?.admin?.components?.edit?.PreviewButton
const PreviewButton = PreviewButtonComponent ? <PreviewButtonComponent /> : undefined */
const PreviewButtonComponent = collectionConfig?.admin?.components?.edit?.PreviewButton
const PreviewButton = PreviewButtonComponent ? <PreviewButtonComponent /> : undefined
const PublishButtonComponent = collectionConfig?.admin?.components?.edit?.PublishButton
const PublishButton = PublishButtonComponent ? <PublishButtonComponent /> : undefined
@@ -111,7 +111,7 @@ export const buildComponentMap = (args: {
BeforeListTable,
Edit: <Edit collectionSlug={collectionConfig.slug} />,
List: <List collectionSlug={collectionConfig.slug} />,
/* PreviewButton, */
PreviewButton,
PublishButton,
SaveButton,
SaveDraftButton,
@@ -123,6 +123,7 @@ export const buildComponentMap = (args: {
fieldSchema: fields,
readOnly: readOnlyOverride,
}),
isPreviewEnabled: !!collectionConfig?.admin?.preview,
}
return {
@@ -143,8 +144,8 @@ export const buildComponentMap = (args: {
const SaveDraftButton = globalConfig?.admin?.components?.elements?.SaveDraftButton
const SaveDraftButtonComponent = SaveDraftButton ? <SaveDraftButton /> : undefined
/* const PreviewButton = globalConfig?.admin?.components?.elements?.PreviewButton
const PreviewButtonComponent = PreviewButton ? <PreviewButton /> : undefined */
const PreviewButton = globalConfig?.admin?.components?.elements?.PreviewButton
const PreviewButtonComponent = PreviewButton ? <PreviewButton /> : undefined
const PublishButton = globalConfig?.admin?.components?.elements?.PublishButton
const PublishButtonComponent = PublishButton ? <PublishButton /> : undefined
@@ -164,7 +165,7 @@ export const buildComponentMap = (args: {
const componentMap: GlobalComponentMap = {
Edit: <Edit globalSlug={globalConfig.slug} />,
/* PreviewButton: PreviewButtonComponent, */
PreviewButton: PreviewButtonComponent,
PublishButton: PublishButtonComponent,
SaveButton: SaveButtonComponent,
SaveDraftButton: SaveDraftButtonComponent,
@@ -176,6 +177,7 @@ export const buildComponentMap = (args: {
fieldSchema: fields,
readOnly: readOnlyOverride,
}),
isPreviewEnabled: !!globalConfig?.admin?.preview,
}
return {

View File

@@ -103,12 +103,13 @@ export type GlobalComponentMap = ConfigComponentMapBase
export type ConfigComponentMapBase = {
Edit: React.ReactNode
/* PreviewButton: React.ReactNode */
PreviewButton: React.ReactNode
PublishButton: React.ReactNode
SaveButton: React.ReactNode
SaveDraftButton: React.ReactNode
actionsMap: ActionMap
fieldMap: FieldMap
isPreviewEnabled: boolean
}
export type ComponentMap = {