diff --git a/packages/next/src/pages/Edit/Default/index.tsx b/packages/next/src/pages/Edit/Default/index.tsx index ef0c88eb0a..0b9cd97e15 100644 --- a/packages/next/src/pages/Edit/Default/index.tsx +++ b/packages/next/src/pages/Edit/Default/index.tsx @@ -57,7 +57,7 @@ export const DefaultEditView: React.FC = () => { serverURL, } = config - const { getFieldMap } = useComponentMap() + const { componentMap, getFieldMap } = useComponentMap() const collectionConfig = collectionSlug && collections.find((collection) => collection.slug === collectionSlug) @@ -142,6 +142,8 @@ export const DefaultEditView: React.FC = () => { [serverURL, apiRoute, id, operation, schemaPath, collectionSlug, globalSlug], ) + const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`] + return (
@@ -220,11 +222,14 @@ export const DefaultEditView: React.FC = () => { /> )} {upload && ( - + + {RegisterGetThumbnailFunction && } + + )} ) diff --git a/packages/next/src/pages/List/Default/Cell/fields/File/index.tsx b/packages/next/src/pages/List/Default/Cell/fields/File/index.tsx index 09f006d6f0..6c7871ee55 100644 --- a/packages/next/src/pages/List/Default/Cell/fields/File/index.tsx +++ b/packages/next/src/pages/List/Default/Cell/fields/File/index.tsx @@ -11,12 +11,13 @@ const baseClass = 'file' export interface FileCellProps extends CellComponentProps {} export const FileCell: React.FC = ({ cellData, customCellContext, rowData }) => { - const { uploadConfig } = customCellContext + const { collectionSlug, uploadConfig } = customCellContext return (
{ const mimeType: Field = { name: 'mimeType', + type: 'text', admin: { hidden: true, readOnly: true, }, label: 'MIME Type', - type: 'text', } const url: Field = { name: 'url', + type: 'text', admin: { hidden: true, readOnly: true, }, label: 'URL', - type: 'text', } const width: Field = { name: 'width', + type: 'number', admin: { hidden: true, readOnly: true, }, label: labels['upload:width'], - type: 'number', } const height: Field = { name: 'height', + type: 'number', admin: { hidden: true, readOnly: true, }, label: labels['upload:height'], - type: 'number', } const filesize: Field = { name: 'filesize', + type: 'number', admin: { hidden: true, readOnly: true, }, label: labels['upload:fileSize'], - type: 'number', } const filename: Field = { name: 'filename', + type: 'text', admin: { disableBulkEdit: true, hidden: true, @@ -82,7 +83,6 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => { }, index: true, label: labels['upload:fileName'], - type: 'text', unique: true, } @@ -93,10 +93,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => { afterRead: [ ({ data }) => { if (data?.filename) { - if (uploadOptions.staticURL.startsWith('/')) { - return `${config.serverURL}${uploadOptions.staticURL}/${data.filename}` - } - return `${uploadOptions.staticURL}/${data.filename}` + return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${data.filename}` } return undefined @@ -119,11 +116,13 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => { uploadFields = uploadFields.concat([ { name: 'sizes', + type: 'group', admin: { hidden: true, }, fields: uploadOptions.imageSizes.map((size) => ({ name: size.name, + type: 'group', admin: { hidden: true, }, @@ -136,10 +135,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => { const sizeFilename = data?.sizes?.[size.name]?.filename if (sizeFilename) { - if (uploadOptions.staticURL.startsWith('/')) { - return `${config.serverURL}${uploadOptions.staticURL}/${sizeFilename}` - } - return `${uploadOptions.staticURL}/${sizeFilename}` + return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${sizeFilename}` } return null @@ -157,10 +153,8 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => { }, ], label: size.name, - type: 'group', })), label: labels['upload:Sizes'], - type: 'group', }, ]) } diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index da722316ad..6d2997e3fa 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -70,7 +70,7 @@ export type ImageSize = Omit & { export type GetAdminThumbnail = (args: { doc: Record }) => false | null | string export type IncomingUploadType = { - adminThumbnail?: GetAdminThumbnail | string + adminThumbnail?: React.ComponentType | string crop?: boolean disableLocalStorage?: boolean filesRequiredOnCreate?: boolean @@ -88,7 +88,12 @@ export type IncomingUploadType = { } export type Upload = { - adminThumbnail?: GetAdminThumbnail | string + /** + * Represents an admin thumbnail, which can be either a React component or a string. + * - If a string, it should be one of the image size names. + * - If a React component, register a function that generates the thumbnail URL using the `useAdminThumbnail` hook. + **/ + adminThumbnail?: React.ComponentType | string crop?: boolean disableLocalStorage?: boolean filesRequiredOnCreate?: boolean diff --git a/packages/richtext-lexical/src/field/features/upload/component/index.tsx b/packages/richtext-lexical/src/field/features/upload/component/index.tsx index ccb986b1e9..31eaae1d15 100644 --- a/packages/richtext-lexical/src/field/features/upload/component/index.tsx +++ b/packages/richtext-lexical/src/field/features/upload/component/index.tsx @@ -74,7 +74,7 @@ const Component: React.FC = (props) => { { initialParams }, ) - const thumbnailSRC = useThumbnail(relatedCollection, data) + const thumbnailSRC = useThumbnail(relatedCollection.slug, relatedCollection.upload, data) const removeUpload = useCallback(() => { editor.update(() => { diff --git a/packages/richtext-slate/src/field/elements/upload/Element/index.tsx b/packages/richtext-slate/src/field/elements/upload/Element/index.tsx index 07526d5b45..0cecff598c 100644 --- a/packages/richtext-slate/src/field/elements/upload/Element/index.tsx +++ b/packages/richtext-slate/src/field/elements/upload/Element/index.tsx @@ -84,7 +84,7 @@ const Element: React.FC = ({ { initialParams }, ) - const thumbnailSRC = useThumbnail(relatedCollection.upload, data) + const thumbnailSRC = useThumbnail(relatedCollection.slug, relatedCollection.upload, data) const removeUpload = useCallback(() => { const elementPath = ReactEditor.findPath(editor, element) diff --git a/packages/ui/src/elements/FileDetails/index.tsx b/packages/ui/src/elements/FileDetails/index.tsx index f93868c8aa..1e96b61b20 100644 --- a/packages/ui/src/elements/FileDetails/index.tsx +++ b/packages/ui/src/elements/FileDetails/index.tsx @@ -22,7 +22,12 @@ const FileDetails: React.FC = (props) => { return (
- +
= (props) => { const { className = '', + collectionSlug, doc: { filename } = {}, doc, fileSrc, @@ -20,7 +21,7 @@ const Thumbnail: React.FC = (props) => { uploadConfig, } = props - const thumbnailSRC = uploadConfig && doc ? useThumbnail(uploadConfig, doc) : fileSrc + const thumbnailSRC = useThumbnail(collectionSlug, uploadConfig, doc) || fileSrc const [src, setSrc] = useState(thumbnailSRC) const classes = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ') diff --git a/packages/ui/src/elements/Thumbnail/types.ts b/packages/ui/src/elements/Thumbnail/types.ts index dd55ff7957..0807ad4422 100644 --- a/packages/ui/src/elements/Thumbnail/types.ts +++ b/packages/ui/src/elements/Thumbnail/types.ts @@ -2,6 +2,7 @@ import type { SanitizedCollectionConfig } from 'payload/types' export type Props = { className?: string + collectionSlug?: string doc?: Record fileSrc?: string imageCacheTag?: string diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index 77b2f96940..c800ad3d8d 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react' import type { Props } from './types' +import { useClientFunctions } from '../..' import Error from '../../forms/Error' import { useFormSubmitted } from '../../forms/Form/context' import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues' @@ -52,6 +53,7 @@ export const UploadActions = ({ canEdit, showSizePreviews }) => { export const Upload: React.FC = (props) => { const { collectionSlug, initialState, onChange, updatedAt, uploadConfig } = props + const clientFunctions = useClientFunctions() const submitted = useFormSubmitted() const [replacingFile, setReplacingFile] = useState(false) @@ -152,7 +154,10 @@ export const Upload: React.FC = (props) => { {value && (
- +
= (props) => {
    {filteredBlocks?.map((block, index) => { - const { imageAltText, imageURL, labels: blockLabels, slug } = block + const { slug, imageAltText, imageURL, labels: blockLabels } = block return (
  • diff --git a/packages/ui/src/hooks/useAdminThumbnail.tsx b/packages/ui/src/hooks/useAdminThumbnail.tsx new file mode 100644 index 0000000000..f4088ed129 --- /dev/null +++ b/packages/ui/src/hooks/useAdminThumbnail.tsx @@ -0,0 +1,8 @@ +import type { GetAdminThumbnail } from 'payload/types' + +import { useAddClientFunction, useDocumentInfo } from '..' + +export const useAdminThumbnail = (func: GetAdminThumbnail) => { + const { collectionSlug } = useDocumentInfo() + useAddClientFunction(`${collectionSlug}.adminThumbnail`, func) +} diff --git a/packages/ui/src/hooks/useThumbnail.ts b/packages/ui/src/hooks/useThumbnail.ts index f80169aace..1affd98850 100644 --- a/packages/ui/src/hooks/useThumbnail.ts +++ b/packages/ui/src/hooks/useThumbnail.ts @@ -2,28 +2,34 @@ import type { SanitizedCollectionConfig } from 'payload/types' import { isImage } from 'payload/utilities' +import { useComponentMap } from '..' import { useConfig } from '../providers/Config' const absoluteURLPattern = new RegExp('^(?:[a-z]+:)?//', 'i') const base64Pattern = new RegExp(/^data:image\/[a-z]+;base64,/) +// /api/[collectionSlug]/file/[filename] + const useThumbnail = ( + collectionSlug: string, uploadConfig: SanitizedCollectionConfig['upload'], doc: Record, ): false | string => { - const { adminThumbnail, staticURL } = uploadConfig + const { + routes: { api: apiRoute }, + serverURL, + } = useConfig() + const { componentMap } = useComponentMap() + + if (!collectionSlug || !uploadConfig || !doc) return null + + const { adminThumbnail } = uploadConfig const { filename, mimeType, sizes, url } = doc + const thumbnailSrcFunction = componentMap?.[`${collectionSlug}.adminThumbnail`] - const { serverURL } = useConfig() - let pathURL = `${serverURL}${staticURL || ''}` - - if (absoluteURLPattern.test(staticURL)) { - pathURL = staticURL - } - - if (typeof adminThumbnail === 'function') { - const thumbnailURL = adminThumbnail({ doc }) + if (typeof thumbnailSrcFunction === 'function') { + const thumbnailURL = thumbnailSrcFunction({ doc }) if (!thumbnailURL) return false @@ -31,7 +37,7 @@ const useThumbnail = ( return thumbnailURL } - return `${pathURL}/${thumbnailURL}` + return `${serverURL}/${thumbnailURL}` } if (isImage(mimeType as string)) { @@ -39,19 +45,23 @@ const useThumbnail = ( return url as string } - if (sizes?.[adminThumbnail]?.url) { - return sizes[adminThumbnail].url - } + if (typeof adminThumbnail === 'string') { + if (sizes?.[adminThumbnail]?.url) { + return sizes[adminThumbnail].url + } - if (sizes?.[adminThumbnail]?.filename) { - return `${pathURL}/${sizes[adminThumbnail].filename}` + if (sizes?.[adminThumbnail]?.filename) { + return `${serverURL}${apiRoute}/${collectionSlug}/file/${sizes[adminThumbnail].filename}` + } } if (url) { return url as string } - return `${pathURL}/${filename}` + if (typeof filename === 'string') { + return `${serverURL}${apiRoute}/${collectionSlug}/file/${filename}` + } } return false diff --git a/packages/ui/src/utilities/buildComponentMap/index.tsx b/packages/ui/src/utilities/buildComponentMap/index.tsx index 870a1cf225..eb66edc598 100644 --- a/packages/ui/src/utilities/buildComponentMap/index.tsx +++ b/packages/ui/src/utilities/buildComponentMap/index.tsx @@ -97,7 +97,15 @@ export const buildComponentMap = (args: { readOnly: readOnlyOverride, }) + let AdminThumbnail = null + if (typeof collectionConfig?.upload?.adminThumbnail === 'function') { + AdminThumbnail = collectionConfig?.upload?.adminThumbnail + } else if (typeof collectionConfig?.upload?.adminThumbnail === 'string') { + AdminThumbnail = () => collectionConfig?.upload?.adminThumbnail + } + const componentMap: CollectionComponentMap = { + AdminThumbnail, AfterList, AfterListTable, BeforeList, diff --git a/packages/ui/src/utilities/buildComponentMap/types.ts b/packages/ui/src/utilities/buildComponentMap/types.ts index 06c1b2e173..06e951782f 100644 --- a/packages/ui/src/utilities/buildComponentMap/types.ts +++ b/packages/ui/src/utilities/buildComponentMap/types.ts @@ -72,6 +72,7 @@ export type MappedField = { export type FieldMap = MappedField[] export type CollectionComponentMap = ConfigComponentMapBase & { + AdminThumbnail: React.ReactNode AfterList: React.ReactNode AfterListTable: React.ReactNode BeforeList: React.ReactNode diff --git a/test/_community/collections/Media/index.ts b/test/_community/collections/Media/index.ts index 408d73274e..16dd434e72 100644 --- a/test/_community/collections/Media/index.ts +++ b/test/_community/collections/Media/index.ts @@ -1,14 +1,9 @@ -import path from 'path' - import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' export const mediaSlug = 'media' export const MediaCollection: CollectionConfig = { slug: mediaSlug, - // upload: { - // staticDir: path.resolve(__dirname, './media'), - // }, upload: true, access: { read: () => true, diff --git a/test/field-error-states/collections/Upload/index.ts b/test/field-error-states/collections/Upload/index.ts index da72adcf6a..c53d69d519 100644 --- a/test/field-error-states/collections/Upload/index.ts +++ b/test/field-error-states/collections/Upload/index.ts @@ -5,7 +5,7 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collecti const Uploads: CollectionConfig = { slug: 'uploads', upload: { - staticDir: path.resolve(__dirname, './uploads'), + staticDir: path.resolve(process.cwd(), 'test/field-error-states/collections/Upload/uploads'), }, fields: [ { diff --git a/test/field-error-states/tsconfig.json b/test/field-error-states/tsconfig.json index 662b955ff5..244de73b1a 100644 --- a/test/field-error-states/tsconfig.json +++ b/test/field-error-states/tsconfig.json @@ -1,4 +1,5 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { "paths": { "payload/generated-types": ["./payload-types.ts"] diff --git a/test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx b/test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx new file mode 100644 index 0000000000..120c95c84f --- /dev/null +++ b/test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx @@ -0,0 +1,35 @@ +'use client' +import type React from 'react' + +import { useAdminThumbnail } from '../../../../packages/ui/src/hooks/useAdminThumbnail' + +type TypeWithFile = { + filename: string + filesize: number + mimeType: string +} & Record + +function docHasFilename(doc: Record): doc is TypeWithFile { + if (typeof doc === 'object' && 'filename' in doc) { + return true + } + return false +} + +export const adminThumbnailSrc = '/media/image-640x480.png' + +function getThumbnailSrc({ doc }) { + if (docHasFilename(doc)) { + if (doc.mimeType.startsWith('image/')) { + return null // Fallback to default admin thumbnail if image + } + return adminThumbnailSrc // Use custom thumbnail if not image + } + return null +} + +export const RegisterAdminThumbnailFn: React.FC = () => { + void useAdminThumbnail(getThumbnailSrc) + + return null +} diff --git a/test/uploads/collections/admin-thumbnail/index.ts b/test/uploads/collections/admin-thumbnail/index.ts index 17bc29ed1a..4114b4bf0e 100644 --- a/test/uploads/collections/admin-thumbnail/index.ts +++ b/test/uploads/collections/admin-thumbnail/index.ts @@ -2,34 +2,13 @@ import path from 'path' import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' -type TypeWithFile = { - filename: string - filesize: number - mimeType: string -} & Record - -function docHasFilename(doc: Record): doc is TypeWithFile { - if (typeof doc === 'object' && 'filename' in doc) { - return true - } - return false -} - -export const adminThumbnailSrc = '/media/image-640x480.png' +import { RegisterAdminThumbnailFn } from './RegisterThumbnailFn' export const AdminThumbnailCol: CollectionConfig = { slug: 'admin-thumbnail', upload: { staticDir: path.resolve(process.cwd(), 'test/uploads/media'), - // adminThumbnail: ({ doc }) => { - // if (docHasFilename(doc)) { - // if (doc.mimeType.startsWith('image/')) { - // return null // Fallback to default admin thumbnail if image - // } - // return adminThumbnailSrc // Use custom thumbnail if not image - // } - // return null - // }, + adminThumbnail: RegisterAdminThumbnailFn, }, fields: [], }