From a330fe601749c85bd72ced83105b686f6f7b0fc8 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Wed, 3 Apr 2024 08:49:31 -0400 Subject: [PATCH] chore(ui): simplifies adminThumbnail functionality (#5615) --- .github/workflows/main.yml | 2 +- .../next/src/views/Edit/Default/index.tsx | 7 +- .../src/collections/config/sanitize.ts | 2 +- packages/payload/src/uploads/getBaseFields.ts | 80 ++++++++++++++++--- packages/payload/src/uploads/types.ts | 2 +- .../field/features/upload/component/index.tsx | 4 +- .../field/elements/upload/Element/index.tsx | 4 +- .../ui/src/elements/FileDetails/index.tsx | 3 +- .../Table/DefaultCell/fields/File/index.tsx | 11 ++- .../TableColumns/buildColumnState.tsx | 5 +- .../ui/src/elements/TableColumns/index.tsx | 1 + packages/ui/src/elements/Thumbnail/index.tsx | 58 ++++++++------ .../ui/src/elements/ThumbnailCard/index.tsx | 10 +-- packages/ui/src/fields/Upload/Input.tsx | 14 ++-- packages/ui/src/hooks/useAdminThumbnail.ts | 9 --- packages/ui/src/hooks/useThumbnail.ts | 66 --------------- .../ComponentMap/buildComponentMap/index.tsx | 8 -- .../ComponentMap/buildComponentMap/types.ts | 1 - test/admin/collections/Upload.ts | 16 ++++ test/admin/config.ts | 2 + test/admin/slugs.ts | 35 ++++---- test/helpers/initPayloadE2E.ts | 2 +- test/tsconfig.json | 3 +- .../AdminThumbnailFunction/index.ts | 17 ++++ .../collections/AdminThumbnailSize/index.ts | 29 +++++++ .../admin-thumbnail/RegisterThumbnailFn.tsx | 35 -------- .../collections/admin-thumbnail/index.ts | 17 ---- test/uploads/config.ts | 19 ++++- test/uploads/e2e.spec.ts | 40 ++++++++-- test/uploads/shared.ts | 4 +- tsconfig.json | 4 +- 31 files changed, 273 insertions(+), 237 deletions(-) delete mode 100644 packages/ui/src/hooks/useAdminThumbnail.ts delete mode 100644 packages/ui/src/hooks/useThumbnail.ts create mode 100644 test/admin/collections/Upload.ts create mode 100644 test/uploads/collections/AdminThumbnailFunction/index.ts create mode 100644 test/uploads/collections/AdminThumbnailSize/index.ts delete mode 100644 test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx delete mode 100644 test/uploads/collections/admin-thumbnail/index.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f8c1b9c48..c2bdd10a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -258,7 +258,7 @@ jobs: - plugin-nested-docs - plugin-seo - versions - # - uploads + - uploads steps: - name: Use Node.js 18 diff --git a/packages/next/src/views/Edit/Default/index.tsx b/packages/next/src/views/Edit/Default/index.tsx index e99e5b02e..f0e8c4c6a 100644 --- a/packages/next/src/views/Edit/Default/index.tsx +++ b/packages/next/src/views/Edit/Default/index.tsx @@ -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 (
@@ -248,7 +244,6 @@ export const DefaultEditView: React.FC = () => { )} {upload && ( - {RegisterGetThumbnailFunction && } { + if (filename) { + return `${config.serverURL || ''}${config.routes.api || ''}/${collectionSlug}/file/${filename}` + } + return undefined +} + +type Args = { + collectionConfig?: CollectionConfig + config: Config + doc: Record +} +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 diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index b6a50f5a2..97dfa0c00 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -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 diff --git a/packages/richtext-lexical/src/field/features/upload/component/index.tsx b/packages/richtext-lexical/src/field/features/upload/component/index.tsx index fa2b92698..044356aa8 100644 --- a/packages/richtext-lexical/src/field/features/upload/component/index.tsx +++ b/packages/richtext-lexical/src/field/features/upload/component/index.tsx @@ -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 = (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 = (props) => { >
+ {/* TODO: migrate to use @payloadcms/ui/elements/Thumbnail component */}
{thumbnailSRC ? {data?.filename} : }
diff --git a/packages/richtext-slate/src/field/elements/upload/Element/index.tsx b/packages/richtext-slate/src/field/elements/upload/Element/index.tsx index cf812b917..ab5e80853 100644 --- a/packages/richtext-slate/src/field/elements/upload/Element/index.tsx +++ b/packages/richtext-slate/src/field/elements/upload/Element/index.tsx @@ -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 = ( { 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 = ( >
+ {/* TODO: migrate to use Thumbnail component */}
{thumbnailSRC ? {data?.filename} : }
diff --git a/packages/ui/src/elements/FileDetails/index.tsx b/packages/ui/src/elements/FileDetails/index.tsx index fe22e98ad..aab719fcb 100644 --- a/packages/ui/src/elements/FileDetails/index.tsx +++ b/packages/ui/src/elements/FileDetails/index.tsx @@ -27,7 +27,7 @@ export const FileDetails: React.FC = (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 (
@@ -35,6 +35,7 @@ export const FileDetails: React.FC = (props) => { diff --git a/packages/ui/src/elements/Table/DefaultCell/fields/File/index.tsx b/packages/ui/src/elements/Table/DefaultCell/fields/File/index.tsx index 474d9b561..e073e2d47 100644 --- a/packages/ui/src/elements/Table/DefaultCell/fields/File/index.tsx +++ b/packages/ui/src/elements/Table/DefaultCell/fields/File/index.tsx @@ -10,7 +10,11 @@ const baseClass = 'file' export interface FileCellProps extends DefaultCellComponentProps {} -export const FileCell: React.FC = ({ cellData, customCellContext, rowData }) => { +export const FileCell: React.FC = ({ + cellData: filename, + customCellContext, + rowData, +}) => { const { collectionSlug, uploadConfig } = customCellContext return ( @@ -20,12 +24,13 @@ export const FileCell: React.FC = ({ cellData, customCellContext, collectionSlug={collectionSlug} doc={{ ...rowData, - filename: cellData, + filename, }} + fileSrc={rowData?.thumbnailURL} size="small" uploadConfig={uploadConfig} /> - {String(cellData)} + {String(filename)}
) } diff --git a/packages/ui/src/elements/TableColumns/buildColumnState.tsx b/packages/ui/src/elements/TableColumns/buildColumnState.tsx index 54378b3b2..8f08f0e2b 100644 --- a/packages/ui/src/elements/TableColumns/buildColumnState.tsx +++ b/packages/ui/src/elements/TableColumns/buildColumnState.tsx @@ -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[] 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 diff --git a/packages/ui/src/elements/TableColumns/index.tsx b/packages/ui/src/elements/TableColumns/index.tsx index 1f2199525..132ec0d2c 100644 --- a/packages/ui/src/elements/TableColumns/index.tsx +++ b/packages/ui/src/elements/TableColumns/index.tsx @@ -131,6 +131,7 @@ export const TableColumnsProvider: React.FC = ({ defaultColumns, useAsTitle, listPreferences, + initialColumns, ]) // ///////////////////////////////////// diff --git a/packages/ui/src/elements/Thumbnail/index.tsx b/packages/ui/src/elements/Thumbnail/index.tsx index 597d8a12d..ae79e1e4a 100644 --- a/packages/ui/src/elements/Thumbnail/index.tsx +++ b/packages/ui/src/elements/Thumbnail/index.tsx @@ -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 = (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 ( -
- {src && {filename} - {!src && } +
+ {fileExists === undefined && } + {fileExists && {filename} + {fileExists === false && }
) } diff --git a/packages/ui/src/elements/ThumbnailCard/index.tsx b/packages/ui/src/elements/ThumbnailCard/index.tsx index c03a8f932..406e11046 100644 --- a/packages/ui/src/elements/ThumbnailCard/index.tsx +++ b/packages/ui/src/elements/ThumbnailCard/index.tsx @@ -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 = (props) => { const config = useConfig() - const { i18n, t } = useTranslation() + const { i18n } = useTranslation() const classes = [ baseClass, @@ -59,10 +58,7 @@ export const ThumbnailCard: React.FC = (props) => { return ( ) diff --git a/packages/ui/src/fields/Upload/Input.tsx b/packages/ui/src/fields/Upload/Input.tsx index 433564db1..4c09576d1 100644 --- a/packages/ui/src/fields/Upload/Input.tsx +++ b/packages/ui/src/fields/Upload/Input.tsx @@ -59,7 +59,7 @@ export const UploadInput: React.FC = (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 = (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 = (props) => { /> {collection?.upload && ( - {file && !missingFile && ( + {fileDoc && !missingFile && ( = (props) => { uploadConfig={collection.upload} /> )} - {(!file || missingFile) && ( + {(!fileDoc || missingFile) && (
diff --git a/packages/ui/src/hooks/useAdminThumbnail.ts b/packages/ui/src/hooks/useAdminThumbnail.ts deleted file mode 100644 index 6aa3add0c..000000000 --- a/packages/ui/src/hooks/useAdminThumbnail.ts +++ /dev/null @@ -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) -} diff --git a/packages/ui/src/hooks/useThumbnail.ts b/packages/ui/src/hooks/useThumbnail.ts deleted file mode 100644 index a397c614a..000000000 --- a/packages/ui/src/hooks/useThumbnail.ts +++ /dev/null @@ -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, -): 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 -} diff --git a/packages/ui/src/providers/ComponentMap/buildComponentMap/index.tsx b/packages/ui/src/providers/ComponentMap/buildComponentMap/index.tsx index f7bee4565..1d4c623f6 100644 --- a/packages/ui/src/providers/ComponentMap/buildComponentMap/index.tsx +++ b/packages/ui/src/providers/ComponentMap/buildComponentMap/index.tsx @@ -96,15 +96,7 @@ export const buildComponentMap = (args: { afterListTable?.map((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, diff --git a/packages/ui/src/providers/ComponentMap/buildComponentMap/types.ts b/packages/ui/src/providers/ComponentMap/buildComponentMap/types.ts index 1ec0e022b..34483d05b 100644 --- a/packages/ui/src/providers/ComponentMap/buildComponentMap/types.ts +++ b/packages/ui/src/providers/ComponentMap/buildComponentMap/types.ts @@ -91,7 +91,6 @@ export type ActionMap = { } export type CollectionComponentMap = ConfigComponentMapBase & { - AdminThumbnail: React.ReactNode AfterList: React.ReactNode AfterListTable: React.ReactNode BeforeList: React.ReactNode diff --git a/test/admin/collections/Upload.ts b/test/admin/collections/Upload.ts new file mode 100644 index 000000000..2192ddf92 --- /dev/null +++ b/test/admin/collections/Upload.ts @@ -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', + }, + ], +} diff --git a/test/admin/config.ts b/test/admin/config.ts index 13f93a093..21af5af18 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -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, diff --git a/test/admin/slugs.ts b/test/admin/slugs.ts index f70622a94..311885f8b 100644 --- a/test/admin/slugs.ts +++ b/test/admin/slugs.ts @@ -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, diff --git a/test/helpers/initPayloadE2E.ts b/test/helpers/initPayloadE2E.ts index 178b5e929..ef0b6e67f 100644 --- a/test/helpers/initPayloadE2E.ts +++ b/test/helpers/initPayloadE2E.ts @@ -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' diff --git a/test/tsconfig.json b/test/tsconfig.json index f1e621546..6b11763be 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -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"], diff --git a/test/uploads/collections/AdminThumbnailFunction/index.ts b/test/uploads/collections/AdminThumbnailFunction/index.ts new file mode 100644 index 000000000..600ec9d5c --- /dev/null +++ b/test/uploads/collections/AdminThumbnailFunction/index.ts @@ -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: [], +} diff --git a/test/uploads/collections/AdminThumbnailSize/index.ts b/test/uploads/collections/AdminThumbnailSize/index.ts new file mode 100644 index 000000000..45cade341 --- /dev/null +++ b/test/uploads/collections/AdminThumbnailSize/index.ts @@ -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: [], +} diff --git a/test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx b/test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx deleted file mode 100644 index 32405996c..000000000 --- a/test/uploads/collections/admin-thumbnail/RegisterThumbnailFn.tsx +++ /dev/null @@ -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 - -function docHasFilename(doc: Record): doc is TypeWithFile { - if (typeof doc === 'object' && 'filename' in doc) { - return true - } - return false -} - -export const adminThumbnailSrc = '/media/image-640x480.png' - -function getThumbnailSrc({ doc }) { - if (docHasFilename(doc)) { - if (doc.mimeType.startsWith('image/')) { - return null // Fallback to default admin thumbnail if image - } - return adminThumbnailSrc // Use custom thumbnail if not image - } - return null -} - -export const RegisterAdminThumbnailFn: React.FC = () => { - void useAdminThumbnail(getThumbnailSrc) - - return null -} diff --git a/test/uploads/collections/admin-thumbnail/index.ts b/test/uploads/collections/admin-thumbnail/index.ts deleted file mode 100644 index 807a15e30..000000000 --- a/test/uploads/collections/admin-thumbnail/index.ts +++ /dev/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: [], -} diff --git a/test/uploads/config.ts b/test/uploads/config.ts index a66b4b70c..20321c1d2 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -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}`, + }, + }) }, }) diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index 0fce5c8ea..595cae49b 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -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') diff --git a/test/uploads/shared.ts b/test/uploads/shared.ts index d7f3167b7..301f4bb75 100644 --- a/test/uploads/shared.ts +++ b/test/uploads/shared.ts @@ -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' diff --git a/tsconfig.json b/tsconfig.json index 217f2bd79..64ddd9d30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" ] -} \ No newline at end of file +}