chore(ui): simplifies adminThumbnail functionality (#5615)

This commit is contained in:
Jarrod Flesch
2024-04-03 08:49:31 -04:00
committed by GitHub
parent 4ee4ad25b0
commit a330fe6017
31 changed files with 273 additions and 237 deletions

View File

@@ -258,7 +258,7 @@ jobs:
- plugin-nested-docs
- plugin-seo
- versions
# - uploads
- uploads
steps:
- name: Use Node.js 18

View File

@@ -59,7 +59,7 @@ export const DefaultEditView: React.FC = () => {
const config = useConfig()
const router = useRouter()
const { dispatchFormQueryParams } = useFormQueryParams()
const { getComponentMap, getFieldMap } = useComponentMap()
const { getFieldMap } = useComponentMap()
const params = useSearchParams()
const depth = useEditDepth()
const { reportUpdate } = useDocumentEvents()
@@ -74,8 +74,6 @@ export const DefaultEditView: React.FC = () => {
const locale = params.get('locale')
const componentMap = getComponentMap({ collectionSlug, globalSlug })
const collectionConfig =
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
@@ -178,8 +176,6 @@ export const DefaultEditView: React.FC = () => {
[serverURL, apiRoute, id, operation, entitySlug, collectionSlug, globalSlug, getDocPreferences],
)
const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`]
return (
<main className={classes}>
<OperationProvider operation={operation}>
@@ -248,7 +244,6 @@ export const DefaultEditView: React.FC = () => {
)}
{upload && (
<React.Fragment>
{RegisterGetThumbnailFunction && <RegisterGetThumbnailFunction />}
<Upload
collectionSlug={collectionConfig.slug}
initialState={initialState}

View File

@@ -12,7 +12,7 @@ import { sanitizeFields } from '../../fields/config/sanitize.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import mergeBaseFields from '../../fields/mergeBaseFields.js'
import { extractTranslations } from '../../translations/extractTranslations.js'
import getBaseUploadFields from '../../uploads/getBaseFields.js'
import { getBaseUploadFields } from '../../uploads/getBaseFields.js'
import { formatLabels } from '../../utilities/formatLabels.js'
import { isPlainObject } from '../../utilities/isPlainObject.js'
import baseVersionFields from '../../versions/baseFields.js'

View File

@@ -14,12 +14,54 @@ const labels = extractTranslations([
'upload:sizes',
])
type GenerateURLArgs = {
collectionSlug: string
config: Config
filename?: string
}
const generateURL = ({ collectionSlug, config, filename }: GenerateURLArgs) => {
if (filename) {
return `${config.serverURL || ''}${config.routes.api || ''}/${collectionSlug}/file/${filename}`
}
return undefined
}
type Args = {
collectionConfig?: CollectionConfig
config: Config
doc: Record<string, unknown>
}
const generateAdminThumbnails = ({ collectionConfig, config, doc }: Args) => {
const adminThumbnail =
typeof collectionConfig.upload !== 'boolean'
? collectionConfig.upload?.adminThumbnail
: undefined
if (typeof adminThumbnail === 'function') {
return adminThumbnail({ doc })
}
if ('sizes' in doc && doc.sizes?.[adminThumbnail]?.filename) {
return generateURL({
collectionSlug: collectionConfig.slug,
config,
filename: doc.sizes?.[adminThumbnail].filename as string,
})
}
return generateURL({
collectionSlug: collectionConfig.slug,
config,
filename: doc.filename as string,
})
}
type Options = {
collection: CollectionConfig
config: Config
}
const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
export const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
const uploadOptions: UploadConfig = typeof collection.upload === 'object' ? collection.upload : {}
const mimeType: Field = {
@@ -42,6 +84,26 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
label: 'URL',
}
const thumbnailURL: Field = {
name: 'thumbnailURL',
type: 'text',
admin: {
hidden: true,
readOnly: true,
},
hooks: {
afterRead: [
({ data }) =>
generateAdminThumbnails({
collectionConfig: collection,
config,
doc: data,
}),
],
},
label: 'Thumbnail URL',
}
const width: Field = {
name: 'width',
type: 'number',
@@ -90,16 +152,16 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
...url,
hooks: {
afterRead: [
({ data }) => {
if (data?.filename) {
return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${data.filename}`
}
return undefined
},
({ data }) =>
generateURL({
collectionSlug: collection.slug,
config,
filename: data?.filename,
}),
],
},
},
thumbnailURL,
filename,
mimeType,
filesize,
@@ -159,5 +221,3 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
}
return uploadFields
}
export default getBaseUploadFields

View File

@@ -75,7 +75,7 @@ export type UploadConfig = {
* - 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
adminThumbnail?: GetAdminThumbnail | string
crop?: boolean
disableLocalStorage?: boolean
filesRequiredOnCreate?: boolean

View File

@@ -14,7 +14,6 @@ import { DrawerToggler } from '@payloadcms/ui/elements/Drawer'
import { useDrawerSlug } from '@payloadcms/ui/elements/Drawer'
import { File } from '@payloadcms/ui/graphics/File'
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
import { useThumbnail } from '@payloadcms/ui/hooks/useThumbnail'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback, useReducer, useState } from 'react'
@@ -76,7 +75,7 @@ const Component: React.FC<ElementProps> = (props) => {
{ initialParams },
)
const thumbnailSRC = useThumbnail(relatedCollection.slug, relatedCollection.upload, data)
const thumbnailSRC = data?.thumbnailURL
const removeUpload = useCallback(() => {
editor.update(() => {
@@ -109,6 +108,7 @@ const Component: React.FC<ElementProps> = (props) => {
>
<div className={`${baseClass}__card`}>
<div className={`${baseClass}__topRow`}>
{/* TODO: migrate to use @payloadcms/ui/elements/Thumbnail component */}
<div className={`${baseClass}__thumbnail`}>
{thumbnailSRC ? <img alt={data?.filename} src={thumbnailSRC} /> : <File />}
</div>

View File

@@ -10,7 +10,6 @@ import { DrawerToggler, useDrawerSlug } from '@payloadcms/ui/elements/Drawer'
import { useListDrawer } from '@payloadcms/ui/elements/ListDrawer'
import { File } from '@payloadcms/ui/graphics/File'
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
import { useThumbnail } from '@payloadcms/ui/hooks/useThumbnail'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import React, { useCallback, useReducer, useState } from 'react'
@@ -81,7 +80,7 @@ const UploadElement: React.FC<Props & { enabledCollectionSlugs?: string[] }> = (
{ initialParams },
)
const thumbnailSRC = useThumbnail(relatedCollection.slug, relatedCollection.upload, data)
const thumbnailSRC = data?.thumbnailURL
const removeUpload = useCallback(() => {
const elementPath = ReactEditor.findPath(editor, element)
@@ -146,6 +145,7 @@ const UploadElement: React.FC<Props & { enabledCollectionSlugs?: string[] }> = (
>
<div className={`${baseClass}__card`}>
<div className={`${baseClass}__topRow`}>
{/* TODO: migrate to use Thumbnail component */}
<div className={`${baseClass}__thumbnail`}>
{thumbnailSRC ? <img alt={data?.filename} src={thumbnailSRC} /> : <File />}
</div>

View File

@@ -27,7 +27,7 @@ export const FileDetails: React.FC<FileDetailsProps> = (props) => {
const { canEdit, collectionSlug, doc, handleRemove, hasImageSizes, imageCacheTag, uploadConfig } =
props
const { id, filename, filesize, height, mimeType, url, width } = doc
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
return (
<div className={baseClass}>
@@ -35,6 +35,7 @@ export const FileDetails: React.FC<FileDetailsProps> = (props) => {
<Thumbnail
collectionSlug={collectionSlug}
doc={doc}
fileSrc={thumbnailURL}
imageCacheTag={imageCacheTag}
uploadConfig={uploadConfig}
/>

View File

@@ -10,7 +10,11 @@ const baseClass = 'file'
export interface FileCellProps extends DefaultCellComponentProps<any> {}
export const FileCell: React.FC<FileCellProps> = ({ cellData, customCellContext, rowData }) => {
export const FileCell: React.FC<FileCellProps> = ({
cellData: filename,
customCellContext,
rowData,
}) => {
const { collectionSlug, uploadConfig } = customCellContext
return (
@@ -20,12 +24,13 @@ export const FileCell: React.FC<FileCellProps> = ({ cellData, customCellContext,
collectionSlug={collectionSlug}
doc={{
...rowData,
filename: cellData,
filename,
}}
fileSrc={rowData?.thumbnailURL}
size="small"
uploadConfig={uploadConfig}
/>
<span className={`${baseClass}__filename`}>{String(cellData)}</span>
<span className={`${baseClass}__filename`}>{String(filename)}</span>
</div>
)
}

View File

@@ -14,14 +14,15 @@ import { DefaultCell } from '../Table/DefaultCell/index.js'
const fieldIsPresentationalOnly = (field: MappedField): boolean => field.type === 'ui'
export const buildColumnState = (args: {
type Args = {
cellProps: Partial<CellComponentProps>[]
columnPreferences: ColumnPreferences
columns?: string[]
enableRowSelections: boolean
fieldMap: FieldMap
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
}): Column[] => {
}
export const buildColumnState = (args: Args): Column[] => {
const { cellProps, columnPreferences, columns, enableRowSelections, fieldMap, useAsTitle } = args
// swap useAsTitle field to first slot

View File

@@ -131,6 +131,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
defaultColumns,
useAsTitle,
listPreferences,
initialColumns,
])
// /////////////////////////////////////

View File

@@ -1,14 +1,15 @@
'use client'
import React, { useEffect, useState } from 'react'
import React from 'react'
import { File } from '../../graphics/File/index.js'
import { useThumbnail } from '../../hooks/useThumbnail.js'
import './index.scss'
const baseClass = 'thumbnail'
import type { SanitizedCollectionConfig } from 'payload/types'
import { File } from '../../graphics/File/index.js'
import { ShimmerEffect } from '../ShimmerEffect/index.js'
export type ThumbnailProps = {
className?: string
collectionSlug?: string
@@ -19,33 +20,42 @@ export type ThumbnailProps = {
uploadConfig?: SanitizedCollectionConfig['upload']
}
const ThumbnailContext = React.createContext({
className: '',
filename: '',
size: 'medium',
src: '',
})
export const useThumbnailContext = () => React.useContext(ThumbnailContext)
export const Thumbnail: React.FC<ThumbnailProps> = (props) => {
const {
className = '',
collectionSlug,
doc: { filename } = {},
doc,
fileSrc,
imageCacheTag,
size,
uploadConfig,
} = props
const { className = '', doc: { filename } = {}, fileSrc, size } = props
const [fileExists, setFileExists] = React.useState(undefined)
const thumbnailSRC = useThumbnail(collectionSlug, uploadConfig, doc) || fileSrc
const [src, setSrc] = useState(thumbnailSRC)
const classNames = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ')
const classes = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ')
useEffect(() => {
if (thumbnailSRC) {
setSrc(`${thumbnailSRC}${imageCacheTag ? `?${imageCacheTag}` : ''}`)
React.useEffect(() => {
if (!fileSrc) {
setFileExists(false)
return
}
}, [thumbnailSRC, imageCacheTag])
const img = new Image()
img.src = fileSrc
img.onload = () => {
setFileExists(true)
}
img.onerror = () => {
setFileExists(false)
}
}, [fileSrc])
return (
<div className={classes}>
{src && <img alt={filename as string} src={src} />}
{!src && <File />}
<div className={classNames}>
{fileExists === undefined && <ShimmerEffect height="100%" />}
{fileExists && <img alt={filename as string} src={fileSrc} />}
{fileExists === false && <File />}
</div>
)
}

View File

@@ -5,7 +5,6 @@ import React from 'react'
import { useConfig } from '../../providers/Config/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
import { Thumbnail } from '../Thumbnail/index.js'
import './index.scss'
export type ThumbnailCardProps = {
@@ -16,7 +15,7 @@ export type ThumbnailCardProps = {
label?: string
onClick?: () => void
onKeyDown?: () => void
thumbnail?: React.ReactNode
thumbnail: React.ReactNode
}
const baseClass = 'thumbnail-card'
@@ -34,7 +33,7 @@ export const ThumbnailCard: React.FC<ThumbnailCardProps> = (props) => {
const config = useConfig()
const { i18n, t } = useTranslation()
const { i18n } = useTranslation()
const classes = [
baseClass,
@@ -59,10 +58,7 @@ export const ThumbnailCard: React.FC<ThumbnailCardProps> = (props) => {
return (
<button className={classes} onClick={onClick} title={title} type="button">
<div className={`${baseClass}__thumbnail`}>
{thumbnail && thumbnail}
{!thumbnail && collection && doc && <Thumbnail doc={doc} size="expand" />}
</div>
<div className={`${baseClass}__thumbnail`}>{thumbnail}</div>
<div className={`${baseClass}__label`}>{title}</div>
</button>
)

View File

@@ -59,7 +59,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
const { i18n, t } = useTranslation()
const [file, setFile] = useState(undefined)
const [fileDoc, setFileDoc] = useState(undefined)
const [missingFile, setMissingFile] = useState(false)
const [collectionSlugs] = useState([collection?.slug])
@@ -83,16 +83,16 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
})
if (response.ok) {
const json = await response.json()
setFile(json)
setFileDoc(json)
} else {
setMissingFile(true)
setFile(undefined)
setFileDoc(undefined)
}
}
void fetchFile()
} else {
setFile(undefined)
setFileDoc(undefined)
}
}, [value, relationTo, api, serverURL, i18n])
@@ -142,10 +142,10 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
/>
{collection?.upload && (
<React.Fragment>
{file && !missingFile && (
{fileDoc && !missingFile && (
<FileDetails
collectionSlug={relationTo}
doc={file}
doc={fileDoc}
handleRemove={
readOnly
? undefined
@@ -156,7 +156,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
uploadConfig={collection.upload}
/>
)}
{(!file || missingFile) && (
{(!fileDoc || missingFile) && (
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__buttons`}>
<DocumentDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>

View File

@@ -1,9 +0,0 @@
import type { GetAdminThumbnail } from 'payload/types'
import { useAddClientFunction } from '../providers/ClientFunction/index.js'
import { useDocumentInfo } from '../providers/DocumentInfo/index.js'
export const useAdminThumbnail = (func: GetAdminThumbnail) => {
const { collectionSlug } = useDocumentInfo()
useAddClientFunction(`${collectionSlug}.adminThumbnail`, func)
}

View File

@@ -1,66 +0,0 @@
import type { SanitizedCollectionConfig } from 'payload/types'
import { isImage } from 'payload/utilities'
import { useComponentMap } from '../providers/ComponentMap/index.js'
import { useConfig } from '../providers/Config/index.js'
const absoluteURLPattern = /^(?:[a-z]+:)?\/\//i
const base64Pattern = /^data:image\/[a-z]+;base64,/
export const useThumbnail = (
collectionSlug: string,
uploadConfig: SanitizedCollectionConfig['upload'],
doc: Record<string, unknown>,
): false | string => {
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`]
if (typeof thumbnailSrcFunction === 'function') {
const thumbnailURL = thumbnailSrcFunction({ doc })
if (!thumbnailURL) return false
if (absoluteURLPattern.test(thumbnailURL) || base64Pattern.test(thumbnailURL)) {
return thumbnailURL
}
return `${serverURL}/${thumbnailURL}`
}
if (adminThumbnail || isImage(mimeType as string)) {
if (typeof adminThumbnail === 'undefined' && url) {
return url as string
}
if (typeof adminThumbnail === 'string') {
if (sizes?.[adminThumbnail]?.url) {
return sizes[adminThumbnail].url
}
if (sizes?.[adminThumbnail]?.filename) {
return `${serverURL}${apiRoute}/${collectionSlug}/file/${sizes[adminThumbnail].filename}`
}
}
if (url) {
return url as string
}
if (typeof filename === 'string') {
return `${serverURL}${apiRoute}/${collectionSlug}/file/${filename}`
}
}
return false
}

View File

@@ -96,15 +96,7 @@ export const buildComponentMap = (args: {
afterListTable?.map((Component) => <Component />)) ||
null
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,

View File

@@ -91,7 +91,6 @@ export type ActionMap = {
}
export type CollectionComponentMap = ConfigComponentMapBase & {
AdminThumbnail: React.ReactNode
AfterList: React.ReactNode
AfterListTable: React.ReactNode
BeforeList: React.ReactNode

View File

@@ -0,0 +1,16 @@
import type { CollectionConfig } from 'payload/types'
import { uploadCollectionSlug } from '../slugs.js'
export const UploadCollection: CollectionConfig = {
slug: uploadCollectionSlug,
upload: {
adminThumbnail: () => 'https://payloadcms.com/images/universal-truth.jpg',
},
fields: [
{
name: 'title',
type: 'text',
},
],
}

View File

@@ -11,6 +11,7 @@ import { CollectionGroup2B } from './collections/Group2B.js'
import { CollectionHidden } from './collections/Hidden.js'
import { CollectionNoApiView } from './collections/NoApiView.js'
import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js'
import { Users } from './collections/Users.js'
import { AdminButton } from './components/AdminButton/index.js'
import { AfterDashboard } from './components/AfterDashboard/index.js'
@@ -74,6 +75,7 @@ export default buildConfigWithDefaults({
},
},
collections: [
UploadCollection,
Posts,
Users,
CollectionHidden,

View File

@@ -1,14 +1,15 @@
export const usersCollectionSlug = 'users' as const
export const customViews1CollectionSlug = 'custom-views-one' as const
export const customViews2CollectionSlug = 'custom-views-two' as const
export const geoCollectionSlug = 'geo' as const
export const postsCollectionSlug = 'posts' as const
export const group1Collection1Slug = 'group-one-collection-ones' as const
export const group1Collection2Slug = 'group-one-collection-twos' as const
export const group2Collection1Slug = 'group-two-collection-ones' as const
export const group2Collection2Slug = 'group-two-collection-twos' as const
export const hiddenCollectionSlug = 'hidden-collection' as const
export const noApiViewCollectionSlug = 'collection-no-api-view' as const
export const usersCollectionSlug = 'users'
export const customViews1CollectionSlug = 'custom-views-one'
export const customViews2CollectionSlug = 'custom-views-two'
export const geoCollectionSlug = 'geo'
export const postsCollectionSlug = 'posts'
export const group1Collection1Slug = 'group-one-collection-ones'
export const group1Collection2Slug = 'group-one-collection-twos'
export const group2Collection1Slug = 'group-two-collection-ones'
export const group2Collection2Slug = 'group-two-collection-twos'
export const hiddenCollectionSlug = 'hidden-collection'
export const noApiViewCollectionSlug = 'collection-no-api-view'
export const uploadCollectionSlug = 'uploads'
export const collectionSlugs = [
usersCollectionSlug,
customViews1CollectionSlug,
@@ -23,12 +24,12 @@ export const collectionSlugs = [
noApiViewCollectionSlug,
]
export const customGlobalViews1GlobalSlug = 'custom-global-views-one' as const
export const customGlobalViews2GlobalSlug = 'custom-global-views-two' as const
export const globalSlug = 'global' as const
export const group1GlobalSlug = 'group-globals-one' as const
export const group2GlobalSlug = 'group-globals-two' as const
export const hiddenGlobalSlug = 'hidden-global' as const
export const customGlobalViews1GlobalSlug = 'custom-global-views-one'
export const customGlobalViews2GlobalSlug = 'custom-global-views-two'
export const globalSlug = 'global'
export const group1GlobalSlug = 'group-globals-one'
export const group2GlobalSlug = 'group-globals-two'
export const hiddenGlobalSlug = 'hidden-global'
export const noApiViewGlobalSlug = 'global-no-api-view'
export const globalSlugs = [
customGlobalViews1GlobalSlug,

View File

@@ -2,7 +2,7 @@ import { getPayloadHMR } from '@payloadcms/next/utilities'
import { createServer } from 'http'
import nextImport from 'next'
import path from 'path'
import { type Payload } from 'payload'
import { type Payload } from 'payload/types'
import { wait } from 'payload/utilities'
import { parse } from 'url'

View File

@@ -37,7 +37,8 @@
"@payloadcms/ui/templates/*": ["./packages/ui/src/templates/*/index.tsx"],
"@payloadcms/ui/utilities/*": ["./packages/ui/src/utilities/*.ts"],
"@payloadcms/ui/scss": ["./packages/ui/src/scss.scss"],
"@payloadcms/ui/scss/app.scss": ["./packages/ui/src/scss/app.scss"]
"@payloadcms/ui/scss/app.scss": ["./packages/ui/src/scss/app.scss"],
"payload/types": ["./packages/payload/src/exports/types/index.ts"],
}
},
"exclude": ["dist", "build", "node_modules", ".eslintrc.js", "dist/**/*.js", "**/dist/**/*.js"],

View File

@@ -0,0 +1,17 @@
import type { CollectionConfig } from 'payload/types'
import path from 'path'
import { fileURLToPath } from 'url'
import { adminThumbnailFunctionSlug } from '../../shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const AdminThumbnailFunction: CollectionConfig = {
slug: adminThumbnailFunctionSlug,
upload: {
staticDir: path.resolve(dirname, 'test/uploads/media'),
adminThumbnail: () => 'https://payloadcms.com/images/universal-truth.jpg',
},
fields: [],
}

View File

@@ -0,0 +1,29 @@
import type { CollectionConfig } from 'payload/types'
import path from 'path'
import { fileURLToPath } from 'url'
import { adminThumbnailSizeSlug } from '../../shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const AdminThumbnailSize: CollectionConfig = {
slug: adminThumbnailSizeSlug,
upload: {
staticDir: path.resolve(dirname, 'test/uploads/media'),
adminThumbnail: 'small',
imageSizes: [
{
name: 'small',
width: 100,
height: 100,
},
{
name: 'medium',
width: 200,
height: 200,
},
],
},
fields: [],
}

View File

@@ -1,35 +0,0 @@
'use client'
import type React from 'react'
import { useAdminThumbnail } from '@payloadcms/ui/hooks/useAdminThumbnail'
type TypeWithFile = {
filename: string
filesize: number
mimeType: string
} & Record<string, unknown>
function docHasFilename(doc: Record<string, unknown>): 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
}

View File

@@ -1,17 +0,0 @@
import type { CollectionConfig } from 'payload/types'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { RegisterAdminThumbnailFn } from './RegisterThumbnailFn.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const AdminThumbnailCol: CollectionConfig = {
slug: 'admin-thumbnail',
upload: {
staticDir: path.resolve(dirname, '../../media'),
adminThumbnail: RegisterAdminThumbnailFn,
},
fields: [],
}

View File

@@ -5,9 +5,10 @@ import { fileURLToPath } from 'url'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import removeFiles from '../helpers/removeFiles.js'
import { AdminThumbnailFunction } from './collections/AdminThumbnailFunction/index.js'
import { AdminThumbnailSize } from './collections/AdminThumbnailSize/index.js'
import { Uploads1 } from './collections/Upload1/index.js'
import { Uploads2 } from './collections/Upload2/index.js'
import { AdminThumbnailCol } from './collections/admin-thumbnail/index.js'
import {
audioSlug,
enlargeSlug,
@@ -416,7 +417,8 @@ export default buildConfigWithDefaults({
},
Uploads1,
Uploads2,
AdminThumbnailCol,
AdminThumbnailFunction,
AdminThumbnailSize,
{
slug: 'optional-file',
fields: [],
@@ -508,7 +510,7 @@ export default buildConfigWithDefaults({
// Create admin thumbnail media
await payload.create({
collection: AdminThumbnailCol.slug,
collection: AdminThumbnailSize.slug,
data: {},
file: {
...audioFile,
@@ -517,12 +519,21 @@ export default buildConfigWithDefaults({
})
await payload.create({
collection: AdminThumbnailCol.slug,
collection: AdminThumbnailSize.slug,
data: {},
file: {
...imageFile,
name: `thumb-${imageFile.name}`,
},
})
await payload.create({
collection: AdminThumbnailFunction.slug,
data: {},
file: {
...imageFile,
name: `function-image-${imageFile.name}`,
},
})
},
})

View File

@@ -12,7 +12,13 @@ import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
import { RESTClient } from '../helpers/rest.js'
import { adminThumbnailSlug, audioSlug, mediaSlug, relationSlug } from './shared.js'
import {
adminThumbnailFunctionSlug,
adminThumbnailSizeSlug,
audioSlug,
mediaSlug,
relationSlug,
} from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -24,7 +30,8 @@ let serverURL: string
let mediaURL: AdminUrlUtil
let audioURL: AdminUrlUtil
let relationURL: AdminUrlUtil
let adminThumbnailURL: AdminUrlUtil
let adminThumbnailSizeURL: AdminUrlUtil
let adminThumbnailFunctionURL: AdminUrlUtil
describe('uploads', () => {
let page: Page
@@ -39,7 +46,8 @@ describe('uploads', () => {
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
audioURL = new AdminUrlUtil(serverURL, audioSlug)
relationURL = new AdminUrlUtil(serverURL, relationSlug)
adminThumbnailURL = new AdminUrlUtil(serverURL, adminThumbnailSlug)
adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug)
adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -196,6 +204,7 @@ describe('uploads', () => {
test('should restrict mimetype based on filterOptions', async () => {
await page.goto(audioURL.edit(audioDoc.id))
await page.waitForURL(audioURL.edit(audioDoc.id))
// remove the selection and open the list drawer
await page.locator('.file-details__remove').click()
@@ -210,16 +219,31 @@ describe('uploads', () => {
.locator('[id^=doc-drawer_media_2_] .file-field__upload input[type="file"]')
.setInputFiles(path.resolve(dirname, './image.png'))
await page.locator('[id^=doc-drawer_media_2_] button#action-save').click()
await expect(page.locator('.Toastify')).toContainText('successfully')
await expect(page.locator('.Toastify .Toastify__toast--success')).toContainText('successfully')
await page.locator('.Toastify .Toastify__toast--success .Toastify__close-button').click()
// save the document and expect an error
await page.locator('button#action-save').click()
await expect(page.locator('.Toastify')).toContainText('Please correct invalid fields.')
await expect(page.locator('.Toastify .Toastify__toast--error')).toContainText(
'Please correct invalid fields.',
)
})
test('Should execute adminThumbnail and provide thumbnail when set', async () => {
await page.goto(adminThumbnailURL.list)
await page.waitForURL(adminThumbnailURL.list)
test('Should render adminThumbnail when using a function', async () => {
await page.goto(adminThumbnailFunctionURL.list)
await page.waitForURL(adminThumbnailFunctionURL.list)
// Ensure sure false or null shows generic file svg
const genericUploadImage = page.locator('tr.row-1 .thumbnail img')
await expect(genericUploadImage).toHaveAttribute(
'src',
'https://payloadcms.com/images/universal-truth.jpg',
)
})
test('Should render adminThumbnail when using a specific size', async () => {
await page.goto(adminThumbnailSizeURL.list)
await page.waitForURL(adminThumbnailSizeURL.list)
// Ensure sure false or null shows generic file svg
const genericUploadImage = page.locator('tr.row-1 .thumbnail img')

View File

@@ -10,7 +10,9 @@ export const enlargeSlug = 'enlarge'
export const reduceSlug = 'reduce'
export const adminThumbnailSlug = 'admin-thumbnail'
export const adminThumbnailFunctionSlug = 'admin-thumbnail-function'
export const adminThumbnailSizeSlug = 'admin-thumbnail-size'
export const unstoredMediaSlug = 'unstored-media'

View File

@@ -37,7 +37,7 @@
],
"paths": {
"@payload-config": [
"./test/_community/config.ts"
"./test/uploads/config.ts"
],
"@payloadcms/live-preview": [
"./packages/live-preview/src"
@@ -160,4 +160,4 @@
".next/types/**/*.ts",
"scripts/**/*.ts"
]
}
}