diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5907914179..5636990e0b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -229,8 +229,8 @@ jobs: - plugin-nested-docs - plugin-seo # - refresh-permissions - # - uploads - versions + - uploads steps: - name: Use Node.js 18 diff --git a/packages/next/src/routes/rest/collections/preview.ts b/packages/next/src/routes/rest/collections/preview.ts new file mode 100644 index 0000000000..a54e9e9242 --- /dev/null +++ b/packages/next/src/routes/rest/collections/preview.ts @@ -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, + }) +} diff --git a/packages/next/src/routes/rest/globals/preview.ts b/packages/next/src/routes/rest/globals/preview.ts new file mode 100644 index 0000000000..5e0662ccd8 --- /dev/null +++ b/packages/next/src/routes/rest/globals/preview.ts @@ -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, + }) +} diff --git a/packages/next/src/routes/rest/index.ts b/packages/next/src/routes/rest/index.ts index 3825b05b9f..a614c2a562 100644 --- a/packages/next/src/routes/rest/index.ts +++ b/packages/next/src/routes/rest/index.ts @@ -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, diff --git a/packages/payload/package.json b/packages/payload/package.json index 74b8dc9b52..01b8bb439c 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -45,6 +45,7 @@ "@payloadcms/translations": "workspace:*", "@swc-node/core": "^1.13.0", "@swc-node/sourcemap-support": "^0.5.0", + "@types/probe-image-size": "^7.2.4", "bson-objectid": "2.0.4", "conf": "10.2.0", "console-table-printer": "2.11.2", @@ -55,7 +56,6 @@ "find-up": "4.1.0", "get-tsconfig": "^4.7.2", "http-status": "1.6.2", - "image-size": "^1.1.1", "joi": "^17.12.1", "json-schema-to-typescript": "11.0.3", "jsonwebtoken": "9.0.1", @@ -67,6 +67,7 @@ "pino-pretty": "10.2.0", "pirates": "^4.0.6", "pluralize": "8.0.0", + "probe-image-size": "^7.2.3", "sanitize-filename": "1.6.3", "scheduler": "0.23.0", "scmp": "2.1.0" diff --git a/packages/payload/src/admin/elements/PreviewButton.ts b/packages/payload/src/admin/elements/PreviewButton.ts index ced75b8563..3763db6848 100644 --- a/packages/payload/src/admin/elements/PreviewButton.ts +++ b/packages/payload/src/admin/elements/PreviewButton.ts @@ -1,11 +1 @@ -export type CustomPreviewButtonProps = React.ComponentType< - DefaultPreviewButtonProps & { - DefaultButton: React.ComponentType - } -> - -export type DefaultPreviewButtonProps = { - disabled: boolean - label: string - preview: () => void -} +export type CustomPreviewButton = React.ComponentType diff --git a/packages/payload/src/admin/elements/PublishButton.ts b/packages/payload/src/admin/elements/PublishButton.ts index ae18e2bb34..40fb78c03f 100644 --- a/packages/payload/src/admin/elements/PublishButton.ts +++ b/packages/payload/src/admin/elements/PublishButton.ts @@ -1 +1 @@ -export type CustomPublishButtonProps = React.ComponentType +export type CustomPublishButton = React.ComponentType diff --git a/packages/payload/src/admin/elements/SaveButton.ts b/packages/payload/src/admin/elements/SaveButton.ts index db78e84647..81b232cffc 100644 --- a/packages/payload/src/admin/elements/SaveButton.ts +++ b/packages/payload/src/admin/elements/SaveButton.ts @@ -1 +1 @@ -export type CustomSaveButtonProps = React.ComponentType +export type CustomSaveButton = React.ComponentType diff --git a/packages/payload/src/admin/elements/SaveDraftButton.ts b/packages/payload/src/admin/elements/SaveDraftButton.ts index 52d1662d61..6e7c04b996 100644 --- a/packages/payload/src/admin/elements/SaveDraftButton.ts +++ b/packages/payload/src/admin/elements/SaveDraftButton.ts @@ -1 +1 @@ -export type CustomSaveDraftButtonProps = React.ComponentType +export type CustomSaveDraftButton = React.ComponentType diff --git a/packages/payload/src/admin/types.ts b/packages/payload/src/admin/types.ts index 42c66d6752..ff055c7edc 100644 --- a/packages/payload/src/admin/types.ts +++ b/packages/payload/src/admin/types.ts @@ -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, diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index a390232e7b..af27774dfd 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -50,6 +50,7 @@ export const createClientCollectionConfig = (collection: SanitizedCollectionConf if ('upload' in sanitized && typeof sanitized.upload === 'object') { sanitized.upload = { ...sanitized.upload } delete sanitized.upload.handlers + delete sanitized.upload.adminThumbnail } if ('auth' in sanitized && typeof sanitized.auth === 'object') { diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 0bc76e44f4..d9d594717e 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -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?: { /** diff --git a/packages/payload/src/globals/config/types.ts b/packages/payload/src/globals/config/types.ts index 2e7580833e..cd6bbb87a6 100644 --- a/packages/payload/src/globals/config/types.ts +++ b/packages/payload/src/globals/config/types.ts @@ -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?: { /** diff --git a/packages/payload/src/uploads/getImageSize.ts b/packages/payload/src/uploads/getImageSize.ts index 8960b0f0ab..82450e3030 100644 --- a/packages/payload/src/uploads/getImageSize.ts +++ b/packages/payload/src/uploads/getImageSize.ts @@ -1,15 +1,14 @@ -import { imageSize } from 'image-size' +import fs from 'fs' +import probeImageSize from 'probe-image-size' import type { PayloadRequest } from '../types/index.js' import type { ProbedImageSize } from './types.js' export function getImageSize(file: PayloadRequest['file']): ProbedImageSize { if (file.tempFilePath) { - const dimensions = imageSize(file.tempFilePath) - return { height: dimensions.height, width: dimensions.width } + const data = fs.readFileSync(file.tempFilePath) + return probeImageSize.sync(data) } - const buffer = Buffer.from(file.data) - const dimensions = imageSize(buffer) - return { height: dimensions.height, width: dimensions.width } + return probeImageSize.sync(file.data) } diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index f8b57898ea..e20d9f72b7 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -154,15 +154,9 @@ export const DocumentControls: React.FC<{
- {/* {(collectionConfig?.admin?.preview || globalConfig?.admin?.preview) && ( - - )} */} + {componentMap?.isPreviewEnabled && ( + + )} {hasSavePermission && ( {collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts ? ( diff --git a/packages/ui/src/elements/PreviewButton/index.tsx b/packages/ui/src/elements/PreviewButton/index.tsx index 357e2d0add..fb7dd3fe19 100644 --- a/packages/ui/src/elements/PreviewButton/index.tsx +++ b/packages/ui/src/elements/PreviewButton/index.tsx @@ -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 = ({ - disabled, - label, - preview, -}) => { +const DefaultPreviewButton: React.FC = () => { + const { generatePreviewURL, label } = usePreviewURL() + return (