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-nested-docs
|
||||||
- plugin-seo
|
- plugin-seo
|
||||||
- versions
|
- versions
|
||||||
# - uploads
|
- uploads
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Use Node.js 18
|
- name: Use Node.js 18
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { dispatchFormQueryParams } = useFormQueryParams()
|
const { dispatchFormQueryParams } = useFormQueryParams()
|
||||||
const { getComponentMap, getFieldMap } = useComponentMap()
|
const { getFieldMap } = useComponentMap()
|
||||||
const params = useSearchParams()
|
const params = useSearchParams()
|
||||||
const depth = useEditDepth()
|
const depth = useEditDepth()
|
||||||
const { reportUpdate } = useDocumentEvents()
|
const { reportUpdate } = useDocumentEvents()
|
||||||
@@ -74,8 +74,6 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
|
|
||||||
const locale = params.get('locale')
|
const locale = params.get('locale')
|
||||||
|
|
||||||
const componentMap = getComponentMap({ collectionSlug, globalSlug })
|
|
||||||
|
|
||||||
const collectionConfig =
|
const collectionConfig =
|
||||||
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
|
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
|
||||||
|
|
||||||
@@ -178,8 +176,6 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
[serverURL, apiRoute, id, operation, entitySlug, collectionSlug, globalSlug, getDocPreferences],
|
[serverURL, apiRoute, id, operation, entitySlug, collectionSlug, globalSlug, getDocPreferences],
|
||||||
)
|
)
|
||||||
|
|
||||||
const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={classes}>
|
<main className={classes}>
|
||||||
<OperationProvider operation={operation}>
|
<OperationProvider operation={operation}>
|
||||||
@@ -248,7 +244,6 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{upload && (
|
{upload && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{RegisterGetThumbnailFunction && <RegisterGetThumbnailFunction />}
|
|
||||||
<Upload
|
<Upload
|
||||||
collectionSlug={collectionConfig.slug}
|
collectionSlug={collectionConfig.slug}
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { sanitizeFields } from '../../fields/config/sanitize.js'
|
|||||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||||
import mergeBaseFields from '../../fields/mergeBaseFields.js'
|
import mergeBaseFields from '../../fields/mergeBaseFields.js'
|
||||||
import { extractTranslations } from '../../translations/extractTranslations.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 { formatLabels } from '../../utilities/formatLabels.js'
|
||||||
import { isPlainObject } from '../../utilities/isPlainObject.js'
|
import { isPlainObject } from '../../utilities/isPlainObject.js'
|
||||||
import baseVersionFields from '../../versions/baseFields.js'
|
import baseVersionFields from '../../versions/baseFields.js'
|
||||||
|
|||||||
@@ -14,12 +14,54 @@ const labels = extractTranslations([
|
|||||||
'upload:sizes',
|
'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 = {
|
type Options = {
|
||||||
collection: CollectionConfig
|
collection: CollectionConfig
|
||||||
config: Config
|
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 uploadOptions: UploadConfig = typeof collection.upload === 'object' ? collection.upload : {}
|
||||||
|
|
||||||
const mimeType: Field = {
|
const mimeType: Field = {
|
||||||
@@ -42,6 +84,26 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
|||||||
label: 'URL',
|
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 = {
|
const width: Field = {
|
||||||
name: 'width',
|
name: 'width',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -90,16 +152,16 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
|||||||
...url,
|
...url,
|
||||||
hooks: {
|
hooks: {
|
||||||
afterRead: [
|
afterRead: [
|
||||||
({ data }) => {
|
({ data }) =>
|
||||||
if (data?.filename) {
|
generateURL({
|
||||||
return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${data.filename}`
|
collectionSlug: collection.slug,
|
||||||
}
|
config,
|
||||||
|
filename: data?.filename,
|
||||||
return undefined
|
}),
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
thumbnailURL,
|
||||||
filename,
|
filename,
|
||||||
mimeType,
|
mimeType,
|
||||||
filesize,
|
filesize,
|
||||||
@@ -159,5 +221,3 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
|
|||||||
}
|
}
|
||||||
return uploadFields
|
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 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.
|
* - 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
|
crop?: boolean
|
||||||
disableLocalStorage?: boolean
|
disableLocalStorage?: boolean
|
||||||
filesRequiredOnCreate?: boolean
|
filesRequiredOnCreate?: boolean
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { DrawerToggler } from '@payloadcms/ui/elements/Drawer'
|
|||||||
import { useDrawerSlug } from '@payloadcms/ui/elements/Drawer'
|
import { useDrawerSlug } from '@payloadcms/ui/elements/Drawer'
|
||||||
import { File } from '@payloadcms/ui/graphics/File'
|
import { File } from '@payloadcms/ui/graphics/File'
|
||||||
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
|
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
|
||||||
import { useThumbnail } from '@payloadcms/ui/hooks/useThumbnail'
|
|
||||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||||
import React, { useCallback, useReducer, useState } from 'react'
|
import React, { useCallback, useReducer, useState } from 'react'
|
||||||
@@ -76,7 +75,7 @@ const Component: React.FC<ElementProps> = (props) => {
|
|||||||
{ initialParams },
|
{ initialParams },
|
||||||
)
|
)
|
||||||
|
|
||||||
const thumbnailSRC = useThumbnail(relatedCollection.slug, relatedCollection.upload, data)
|
const thumbnailSRC = data?.thumbnailURL
|
||||||
|
|
||||||
const removeUpload = useCallback(() => {
|
const removeUpload = useCallback(() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
@@ -109,6 +108,7 @@ const Component: React.FC<ElementProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<div className={`${baseClass}__card`}>
|
<div className={`${baseClass}__card`}>
|
||||||
<div className={`${baseClass}__topRow`}>
|
<div className={`${baseClass}__topRow`}>
|
||||||
|
{/* TODO: migrate to use @payloadcms/ui/elements/Thumbnail component */}
|
||||||
<div className={`${baseClass}__thumbnail`}>
|
<div className={`${baseClass}__thumbnail`}>
|
||||||
{thumbnailSRC ? <img alt={data?.filename} src={thumbnailSRC} /> : <File />}
|
{thumbnailSRC ? <img alt={data?.filename} src={thumbnailSRC} /> : <File />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { DrawerToggler, useDrawerSlug } from '@payloadcms/ui/elements/Drawer'
|
|||||||
import { useListDrawer } from '@payloadcms/ui/elements/ListDrawer'
|
import { useListDrawer } from '@payloadcms/ui/elements/ListDrawer'
|
||||||
import { File } from '@payloadcms/ui/graphics/File'
|
import { File } from '@payloadcms/ui/graphics/File'
|
||||||
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
|
import usePayloadAPI from '@payloadcms/ui/hooks/usePayloadAPI'
|
||||||
import { useThumbnail } from '@payloadcms/ui/hooks/useThumbnail'
|
|
||||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||||
import React, { useCallback, useReducer, useState } from 'react'
|
import React, { useCallback, useReducer, useState } from 'react'
|
||||||
@@ -81,7 +80,7 @@ const UploadElement: React.FC<Props & { enabledCollectionSlugs?: string[] }> = (
|
|||||||
{ initialParams },
|
{ initialParams },
|
||||||
)
|
)
|
||||||
|
|
||||||
const thumbnailSRC = useThumbnail(relatedCollection.slug, relatedCollection.upload, data)
|
const thumbnailSRC = data?.thumbnailURL
|
||||||
|
|
||||||
const removeUpload = useCallback(() => {
|
const removeUpload = useCallback(() => {
|
||||||
const elementPath = ReactEditor.findPath(editor, element)
|
const elementPath = ReactEditor.findPath(editor, element)
|
||||||
@@ -146,6 +145,7 @@ const UploadElement: React.FC<Props & { enabledCollectionSlugs?: string[] }> = (
|
|||||||
>
|
>
|
||||||
<div className={`${baseClass}__card`}>
|
<div className={`${baseClass}__card`}>
|
||||||
<div className={`${baseClass}__topRow`}>
|
<div className={`${baseClass}__topRow`}>
|
||||||
|
{/* TODO: migrate to use Thumbnail component */}
|
||||||
<div className={`${baseClass}__thumbnail`}>
|
<div className={`${baseClass}__thumbnail`}>
|
||||||
{thumbnailSRC ? <img alt={data?.filename} src={thumbnailSRC} /> : <File />}
|
{thumbnailSRC ? <img alt={data?.filename} src={thumbnailSRC} /> : <File />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const FileDetails: React.FC<FileDetailsProps> = (props) => {
|
|||||||
const { canEdit, collectionSlug, doc, handleRemove, hasImageSizes, imageCacheTag, uploadConfig } =
|
const { canEdit, collectionSlug, doc, handleRemove, hasImageSizes, imageCacheTag, uploadConfig } =
|
||||||
props
|
props
|
||||||
|
|
||||||
const { id, filename, filesize, height, mimeType, url, width } = doc
|
const { id, filename, filesize, height, mimeType, thumbnailURL, url, width } = doc
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
@@ -35,6 +35,7 @@ export const FileDetails: React.FC<FileDetailsProps> = (props) => {
|
|||||||
<Thumbnail
|
<Thumbnail
|
||||||
collectionSlug={collectionSlug}
|
collectionSlug={collectionSlug}
|
||||||
doc={doc}
|
doc={doc}
|
||||||
|
fileSrc={thumbnailURL}
|
||||||
imageCacheTag={imageCacheTag}
|
imageCacheTag={imageCacheTag}
|
||||||
uploadConfig={uploadConfig}
|
uploadConfig={uploadConfig}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ const baseClass = 'file'
|
|||||||
|
|
||||||
export interface FileCellProps extends DefaultCellComponentProps<any> {}
|
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
|
const { collectionSlug, uploadConfig } = customCellContext
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,12 +24,13 @@ export const FileCell: React.FC<FileCellProps> = ({ cellData, customCellContext,
|
|||||||
collectionSlug={collectionSlug}
|
collectionSlug={collectionSlug}
|
||||||
doc={{
|
doc={{
|
||||||
...rowData,
|
...rowData,
|
||||||
filename: cellData,
|
filename,
|
||||||
}}
|
}}
|
||||||
|
fileSrc={rowData?.thumbnailURL}
|
||||||
size="small"
|
size="small"
|
||||||
uploadConfig={uploadConfig}
|
uploadConfig={uploadConfig}
|
||||||
/>
|
/>
|
||||||
<span className={`${baseClass}__filename`}>{String(cellData)}</span>
|
<span className={`${baseClass}__filename`}>{String(filename)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ import { DefaultCell } from '../Table/DefaultCell/index.js'
|
|||||||
|
|
||||||
const fieldIsPresentationalOnly = (field: MappedField): boolean => field.type === 'ui'
|
const fieldIsPresentationalOnly = (field: MappedField): boolean => field.type === 'ui'
|
||||||
|
|
||||||
export const buildColumnState = (args: {
|
type Args = {
|
||||||
cellProps: Partial<CellComponentProps>[]
|
cellProps: Partial<CellComponentProps>[]
|
||||||
columnPreferences: ColumnPreferences
|
columnPreferences: ColumnPreferences
|
||||||
columns?: string[]
|
columns?: string[]
|
||||||
enableRowSelections: boolean
|
enableRowSelections: boolean
|
||||||
fieldMap: FieldMap
|
fieldMap: FieldMap
|
||||||
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
|
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
|
||||||
}): Column[] => {
|
}
|
||||||
|
export const buildColumnState = (args: Args): Column[] => {
|
||||||
const { cellProps, columnPreferences, columns, enableRowSelections, fieldMap, useAsTitle } = args
|
const { cellProps, columnPreferences, columns, enableRowSelections, fieldMap, useAsTitle } = args
|
||||||
|
|
||||||
// swap useAsTitle field to first slot
|
// swap useAsTitle field to first slot
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export const TableColumnsProvider: React.FC<Props> = ({
|
|||||||
defaultColumns,
|
defaultColumns,
|
||||||
useAsTitle,
|
useAsTitle,
|
||||||
listPreferences,
|
listPreferences,
|
||||||
|
initialColumns,
|
||||||
])
|
])
|
||||||
|
|
||||||
// /////////////////////////////////////
|
// /////////////////////////////////////
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
'use client'
|
'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'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'thumbnail'
|
const baseClass = 'thumbnail'
|
||||||
|
|
||||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { File } from '../../graphics/File/index.js'
|
||||||
|
import { ShimmerEffect } from '../ShimmerEffect/index.js'
|
||||||
|
|
||||||
export type ThumbnailProps = {
|
export type ThumbnailProps = {
|
||||||
className?: string
|
className?: string
|
||||||
collectionSlug?: string
|
collectionSlug?: string
|
||||||
@@ -19,33 +20,42 @@ export type ThumbnailProps = {
|
|||||||
uploadConfig?: SanitizedCollectionConfig['upload']
|
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) => {
|
export const Thumbnail: React.FC<ThumbnailProps> = (props) => {
|
||||||
const {
|
const { className = '', doc: { filename } = {}, fileSrc, size } = props
|
||||||
className = '',
|
const [fileExists, setFileExists] = React.useState(undefined)
|
||||||
collectionSlug,
|
|
||||||
doc: { filename } = {},
|
|
||||||
doc,
|
|
||||||
fileSrc,
|
|
||||||
imageCacheTag,
|
|
||||||
size,
|
|
||||||
uploadConfig,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const thumbnailSRC = useThumbnail(collectionSlug, uploadConfig, doc) || fileSrc
|
const classNames = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ')
|
||||||
const [src, setSrc] = useState(thumbnailSRC)
|
|
||||||
|
|
||||||
const classes = [baseClass, `${baseClass}--size-${size || 'medium'}`, className].join(' ')
|
React.useEffect(() => {
|
||||||
|
if (!fileSrc) {
|
||||||
useEffect(() => {
|
setFileExists(false)
|
||||||
if (thumbnailSRC) {
|
return
|
||||||
setSrc(`${thumbnailSRC}${imageCacheTag ? `?${imageCacheTag}` : ''}`)
|
|
||||||
}
|
}
|
||||||
}, [thumbnailSRC, imageCacheTag])
|
|
||||||
|
const img = new Image()
|
||||||
|
img.src = fileSrc
|
||||||
|
img.onload = () => {
|
||||||
|
setFileExists(true)
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
setFileExists(false)
|
||||||
|
}
|
||||||
|
}, [fileSrc])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classNames}>
|
||||||
{src && <img alt={filename as string} src={src} />}
|
{fileExists === undefined && <ShimmerEffect height="100%" />}
|
||||||
{!src && <File />}
|
{fileExists && <img alt={filename as string} src={fileSrc} />}
|
||||||
|
{fileExists === false && <File />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import React from 'react'
|
|||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
||||||
import { Thumbnail } from '../Thumbnail/index.js'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
export type ThumbnailCardProps = {
|
export type ThumbnailCardProps = {
|
||||||
@@ -16,7 +15,7 @@ export type ThumbnailCardProps = {
|
|||||||
label?: string
|
label?: string
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
onKeyDown?: () => void
|
onKeyDown?: () => void
|
||||||
thumbnail?: React.ReactNode
|
thumbnail: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseClass = 'thumbnail-card'
|
const baseClass = 'thumbnail-card'
|
||||||
@@ -34,7 +33,7 @@ export const ThumbnailCard: React.FC<ThumbnailCardProps> = (props) => {
|
|||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClass,
|
baseClass,
|
||||||
@@ -59,10 +58,7 @@ export const ThumbnailCard: React.FC<ThumbnailCardProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={classes} onClick={onClick} title={title} type="button">
|
<button className={classes} onClick={onClick} title={title} type="button">
|
||||||
<div className={`${baseClass}__thumbnail`}>
|
<div className={`${baseClass}__thumbnail`}>{thumbnail}</div>
|
||||||
{thumbnail && thumbnail}
|
|
||||||
{!thumbnail && collection && doc && <Thumbnail doc={doc} size="expand" />}
|
|
||||||
</div>
|
|
||||||
<div className={`${baseClass}__label`}>{title}</div>
|
<div className={`${baseClass}__label`}>{title}</div>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
|
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
|
||||||
const [file, setFile] = useState(undefined)
|
const [fileDoc, setFileDoc] = useState(undefined)
|
||||||
const [missingFile, setMissingFile] = useState(false)
|
const [missingFile, setMissingFile] = useState(false)
|
||||||
const [collectionSlugs] = useState([collection?.slug])
|
const [collectionSlugs] = useState([collection?.slug])
|
||||||
|
|
||||||
@@ -83,16 +83,16 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
})
|
})
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
setFile(json)
|
setFileDoc(json)
|
||||||
} else {
|
} else {
|
||||||
setMissingFile(true)
|
setMissingFile(true)
|
||||||
setFile(undefined)
|
setFileDoc(undefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void fetchFile()
|
void fetchFile()
|
||||||
} else {
|
} else {
|
||||||
setFile(undefined)
|
setFileDoc(undefined)
|
||||||
}
|
}
|
||||||
}, [value, relationTo, api, serverURL, i18n])
|
}, [value, relationTo, api, serverURL, i18n])
|
||||||
|
|
||||||
@@ -142,10 +142,10 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
{collection?.upload && (
|
{collection?.upload && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{file && !missingFile && (
|
{fileDoc && !missingFile && (
|
||||||
<FileDetails
|
<FileDetails
|
||||||
collectionSlug={relationTo}
|
collectionSlug={relationTo}
|
||||||
doc={file}
|
doc={fileDoc}
|
||||||
handleRemove={
|
handleRemove={
|
||||||
readOnly
|
readOnly
|
||||||
? undefined
|
? undefined
|
||||||
@@ -156,7 +156,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
uploadConfig={collection.upload}
|
uploadConfig={collection.upload}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(!file || missingFile) && (
|
{(!fileDoc || missingFile) && (
|
||||||
<div className={`${baseClass}__wrap`}>
|
<div className={`${baseClass}__wrap`}>
|
||||||
<div className={`${baseClass}__buttons`}>
|
<div className={`${baseClass}__buttons`}>
|
||||||
<DocumentDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
|
<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 />)) ||
|
afterListTable?.map((Component) => <Component />)) ||
|
||||||
null
|
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 = {
|
const componentMap: CollectionComponentMap = {
|
||||||
AdminThumbnail,
|
|
||||||
AfterList,
|
AfterList,
|
||||||
AfterListTable,
|
AfterListTable,
|
||||||
BeforeList,
|
BeforeList,
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export type ActionMap = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CollectionComponentMap = ConfigComponentMapBase & {
|
export type CollectionComponentMap = ConfigComponentMapBase & {
|
||||||
AdminThumbnail: React.ReactNode
|
|
||||||
AfterList: React.ReactNode
|
AfterList: React.ReactNode
|
||||||
AfterListTable: React.ReactNode
|
AfterListTable: React.ReactNode
|
||||||
BeforeList: 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 { CollectionHidden } from './collections/Hidden.js'
|
||||||
import { CollectionNoApiView } from './collections/NoApiView.js'
|
import { CollectionNoApiView } from './collections/NoApiView.js'
|
||||||
import { Posts } from './collections/Posts.js'
|
import { Posts } from './collections/Posts.js'
|
||||||
|
import { UploadCollection } from './collections/Upload.js'
|
||||||
import { Users } from './collections/Users.js'
|
import { Users } from './collections/Users.js'
|
||||||
import { AdminButton } from './components/AdminButton/index.js'
|
import { AdminButton } from './components/AdminButton/index.js'
|
||||||
import { AfterDashboard } from './components/AfterDashboard/index.js'
|
import { AfterDashboard } from './components/AfterDashboard/index.js'
|
||||||
@@ -74,6 +75,7 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
collections: [
|
collections: [
|
||||||
|
UploadCollection,
|
||||||
Posts,
|
Posts,
|
||||||
Users,
|
Users,
|
||||||
CollectionHidden,
|
CollectionHidden,
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
export const usersCollectionSlug = 'users' as const
|
export const usersCollectionSlug = 'users'
|
||||||
export const customViews1CollectionSlug = 'custom-views-one' as const
|
export const customViews1CollectionSlug = 'custom-views-one'
|
||||||
export const customViews2CollectionSlug = 'custom-views-two' as const
|
export const customViews2CollectionSlug = 'custom-views-two'
|
||||||
export const geoCollectionSlug = 'geo' as const
|
export const geoCollectionSlug = 'geo'
|
||||||
export const postsCollectionSlug = 'posts' as const
|
export const postsCollectionSlug = 'posts'
|
||||||
export const group1Collection1Slug = 'group-one-collection-ones' as const
|
export const group1Collection1Slug = 'group-one-collection-ones'
|
||||||
export const group1Collection2Slug = 'group-one-collection-twos' as const
|
export const group1Collection2Slug = 'group-one-collection-twos'
|
||||||
export const group2Collection1Slug = 'group-two-collection-ones' as const
|
export const group2Collection1Slug = 'group-two-collection-ones'
|
||||||
export const group2Collection2Slug = 'group-two-collection-twos' as const
|
export const group2Collection2Slug = 'group-two-collection-twos'
|
||||||
export const hiddenCollectionSlug = 'hidden-collection' as const
|
export const hiddenCollectionSlug = 'hidden-collection'
|
||||||
export const noApiViewCollectionSlug = 'collection-no-api-view' as const
|
export const noApiViewCollectionSlug = 'collection-no-api-view'
|
||||||
|
export const uploadCollectionSlug = 'uploads'
|
||||||
export const collectionSlugs = [
|
export const collectionSlugs = [
|
||||||
usersCollectionSlug,
|
usersCollectionSlug,
|
||||||
customViews1CollectionSlug,
|
customViews1CollectionSlug,
|
||||||
@@ -23,12 +24,12 @@ export const collectionSlugs = [
|
|||||||
noApiViewCollectionSlug,
|
noApiViewCollectionSlug,
|
||||||
]
|
]
|
||||||
|
|
||||||
export const customGlobalViews1GlobalSlug = 'custom-global-views-one' as const
|
export const customGlobalViews1GlobalSlug = 'custom-global-views-one'
|
||||||
export const customGlobalViews2GlobalSlug = 'custom-global-views-two' as const
|
export const customGlobalViews2GlobalSlug = 'custom-global-views-two'
|
||||||
export const globalSlug = 'global' as const
|
export const globalSlug = 'global'
|
||||||
export const group1GlobalSlug = 'group-globals-one' as const
|
export const group1GlobalSlug = 'group-globals-one'
|
||||||
export const group2GlobalSlug = 'group-globals-two' as const
|
export const group2GlobalSlug = 'group-globals-two'
|
||||||
export const hiddenGlobalSlug = 'hidden-global' as const
|
export const hiddenGlobalSlug = 'hidden-global'
|
||||||
export const noApiViewGlobalSlug = 'global-no-api-view'
|
export const noApiViewGlobalSlug = 'global-no-api-view'
|
||||||
export const globalSlugs = [
|
export const globalSlugs = [
|
||||||
customGlobalViews1GlobalSlug,
|
customGlobalViews1GlobalSlug,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getPayloadHMR } from '@payloadcms/next/utilities'
|
|||||||
import { createServer } from 'http'
|
import { createServer } from 'http'
|
||||||
import nextImport from 'next'
|
import nextImport from 'next'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { type Payload } from 'payload'
|
import { type Payload } from 'payload/types'
|
||||||
import { wait } from 'payload/utilities'
|
import { wait } from 'payload/utilities'
|
||||||
import { parse } from 'url'
|
import { parse } from 'url'
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
"@payloadcms/ui/templates/*": ["./packages/ui/src/templates/*/index.tsx"],
|
"@payloadcms/ui/templates/*": ["./packages/ui/src/templates/*/index.tsx"],
|
||||||
"@payloadcms/ui/utilities/*": ["./packages/ui/src/utilities/*.ts"],
|
"@payloadcms/ui/utilities/*": ["./packages/ui/src/utilities/*.ts"],
|
||||||
"@payloadcms/ui/scss": ["./packages/ui/src/scss.scss"],
|
"@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"],
|
"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 { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
import { devUser } from '../credentials.js'
|
import { devUser } from '../credentials.js'
|
||||||
import removeFiles from '../helpers/removeFiles.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 { Uploads1 } from './collections/Upload1/index.js'
|
||||||
import { Uploads2 } from './collections/Upload2/index.js'
|
import { Uploads2 } from './collections/Upload2/index.js'
|
||||||
import { AdminThumbnailCol } from './collections/admin-thumbnail/index.js'
|
|
||||||
import {
|
import {
|
||||||
audioSlug,
|
audioSlug,
|
||||||
enlargeSlug,
|
enlargeSlug,
|
||||||
@@ -416,7 +417,8 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
Uploads1,
|
Uploads1,
|
||||||
Uploads2,
|
Uploads2,
|
||||||
AdminThumbnailCol,
|
AdminThumbnailFunction,
|
||||||
|
AdminThumbnailSize,
|
||||||
{
|
{
|
||||||
slug: 'optional-file',
|
slug: 'optional-file',
|
||||||
fields: [],
|
fields: [],
|
||||||
@@ -508,7 +510,7 @@ export default buildConfigWithDefaults({
|
|||||||
|
|
||||||
// Create admin thumbnail media
|
// Create admin thumbnail media
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: AdminThumbnailCol.slug,
|
collection: AdminThumbnailSize.slug,
|
||||||
data: {},
|
data: {},
|
||||||
file: {
|
file: {
|
||||||
...audioFile,
|
...audioFile,
|
||||||
@@ -517,12 +519,21 @@ export default buildConfigWithDefaults({
|
|||||||
})
|
})
|
||||||
|
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: AdminThumbnailCol.slug,
|
collection: AdminThumbnailSize.slug,
|
||||||
data: {},
|
data: {},
|
||||||
file: {
|
file: {
|
||||||
...imageFile,
|
...imageFile,
|
||||||
name: `thumb-${imageFile.name}`,
|
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 { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
import { initPayloadE2E } from '../helpers/initPayloadE2E.js'
|
||||||
import { RESTClient } from '../helpers/rest.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 filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
@@ -24,7 +30,8 @@ let serverURL: string
|
|||||||
let mediaURL: AdminUrlUtil
|
let mediaURL: AdminUrlUtil
|
||||||
let audioURL: AdminUrlUtil
|
let audioURL: AdminUrlUtil
|
||||||
let relationURL: AdminUrlUtil
|
let relationURL: AdminUrlUtil
|
||||||
let adminThumbnailURL: AdminUrlUtil
|
let adminThumbnailSizeURL: AdminUrlUtil
|
||||||
|
let adminThumbnailFunctionURL: AdminUrlUtil
|
||||||
|
|
||||||
describe('uploads', () => {
|
describe('uploads', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
@@ -39,7 +46,8 @@ describe('uploads', () => {
|
|||||||
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
|
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
|
||||||
audioURL = new AdminUrlUtil(serverURL, audioSlug)
|
audioURL = new AdminUrlUtil(serverURL, audioSlug)
|
||||||
relationURL = new AdminUrlUtil(serverURL, relationSlug)
|
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()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -196,6 +204,7 @@ describe('uploads', () => {
|
|||||||
|
|
||||||
test('should restrict mimetype based on filterOptions', async () => {
|
test('should restrict mimetype based on filterOptions', async () => {
|
||||||
await page.goto(audioURL.edit(audioDoc.id))
|
await page.goto(audioURL.edit(audioDoc.id))
|
||||||
|
await page.waitForURL(audioURL.edit(audioDoc.id))
|
||||||
|
|
||||||
// remove the selection and open the list drawer
|
// remove the selection and open the list drawer
|
||||||
await page.locator('.file-details__remove').click()
|
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"]')
|
.locator('[id^=doc-drawer_media_2_] .file-field__upload input[type="file"]')
|
||||||
.setInputFiles(path.resolve(dirname, './image.png'))
|
.setInputFiles(path.resolve(dirname, './image.png'))
|
||||||
await page.locator('[id^=doc-drawer_media_2_] button#action-save').click()
|
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
|
// save the document and expect an error
|
||||||
await page.locator('button#action-save').click()
|
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 () => {
|
test('Should render adminThumbnail when using a function', async () => {
|
||||||
await page.goto(adminThumbnailURL.list)
|
await page.goto(adminThumbnailFunctionURL.list)
|
||||||
await page.waitForURL(adminThumbnailURL.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
|
// Ensure sure false or null shows generic file svg
|
||||||
const genericUploadImage = page.locator('tr.row-1 .thumbnail img')
|
const genericUploadImage = page.locator('tr.row-1 .thumbnail img')
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ export const enlargeSlug = 'enlarge'
|
|||||||
|
|
||||||
export const reduceSlug = 'reduce'
|
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'
|
export const unstoredMediaSlug = 'unstored-media'
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": [
|
"@payload-config": [
|
||||||
"./test/_community/config.ts"
|
"./test/uploads/config.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/live-preview": [
|
"@payloadcms/live-preview": [
|
||||||
"./packages/live-preview/src"
|
"./packages/live-preview/src"
|
||||||
@@ -160,4 +160,4 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
"scripts/**/*.ts"
|
"scripts/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user