chore(ui): simplifies adminThumbnail functionality (#5615)
This commit is contained in:
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -258,7 +258,7 @@ jobs:
|
||||
- plugin-nested-docs
|
||||
- plugin-seo
|
||||
- versions
|
||||
# - uploads
|
||||
- uploads
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -131,6 +131,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
|
||||
defaultColumns,
|
||||
useAsTitle,
|
||||
listPreferences,
|
||||
initialColumns,
|
||||
])
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -91,7 +91,6 @@ export type ActionMap = {
|
||||
}
|
||||
|
||||
export type CollectionComponentMap = ConfigComponentMapBase & {
|
||||
AdminThumbnail: React.ReactNode
|
||||
AfterList: React.ReactNode
|
||||
AfterListTable: React.ReactNode
|
||||
BeforeList: React.ReactNode
|
||||
|
||||
16
test/admin/collections/Upload.ts
Normal file
16
test/admin/collections/Upload.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
17
test/uploads/collections/AdminThumbnailFunction/index.ts
Normal file
17
test/uploads/collections/AdminThumbnailFunction/index.ts
Normal 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: [],
|
||||
}
|
||||
29
test/uploads/collections/AdminThumbnailSize/index.ts
Normal file
29
test/uploads/collections/AdminThumbnailSize/index.ts
Normal 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: [],
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: [],
|
||||
}
|
||||
@@ -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}`,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/_community/config.ts"
|
||||
"./test/uploads/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user