From f2e04222f47e7da9614755ed8a4f70d16972f25e Mon Sep 17 00:00:00 2001 From: Kendell Joseph <1900724+kendelljoseph@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:47:46 -0400 Subject: [PATCH] feat: admin upload controls (#11615) ### What? Adds the ability to add additional components to the file upload component. ```ts export const Media: CollectionConfig = { slug: 'media', upload: { admin: { components: { controls: [ '/collections/components/Control/index.js#UploadControl', ], }, }, }, fields: [], } ``` ![image](https://github.com/user-attachments/assets/4706e05b-4e95-4f15-8444-a279c589074e) ### Provider Use the `useUploadControls` provider to either `setUploadControlFile` passing a file object, or set the file by url using `setUploadControlFileUrl`. ```tsx 'use client' import { Button, useUploadControls } from '@payloadcms/ui' import React, { useCallback } from 'react' export const UploadControl = () => { const { setUploadControlFile, setUploadControlFileUrl } = useUploadControls() const loadFromFile = useCallback(async () => { const response = await fetch('https://payloadcms.com/images/universal-truth.jpg') const blob = await response.blob() const file = new File([blob], 'universal-truth.jpg', { type: 'image/jpeg' }) setUploadControlFile(file) }, [setUploadControlFile]) const loadFromUrl = useCallback(() => { setUploadControlFileUrl('https://payloadcms.com/images/universal-truth.jpg') }, [setUploadControlFileUrl]) return (

) } ``` ### Why? Add the ability to use a custom component to select a document to upload. --- .../views/Document/renderDocumentSlots.tsx | 8 ++ packages/payload/src/admin/types.ts | 1 + .../generateImportMap/iterateCollections.ts | 4 + packages/payload/src/uploads/types.ts | 15 +++ .../elements/BulkUpload/EditForm/index.tsx | 2 +- packages/ui/src/elements/BulkUpload/index.tsx | 5 +- packages/ui/src/elements/Upload/index.scss | 1 + packages/ui/src/elements/Upload/index.tsx | 92 +++++++++++++++---- packages/ui/src/exports/client/index.ts | 1 + .../ui/src/providers/UploadControls/index.tsx | 42 +++++++++ packages/ui/src/views/Edit/index.tsx | 21 +++-- .../components/UploadControl/index.client.tsx | 30 ++++++ .../components/UploadControl/index.tsx | 11 +++ .../collections/AdminUploadControl/index.ts | 23 +++++ test/uploads/config.ts | 2 + test/uploads/e2e.spec.ts | 56 ++++++++++- test/uploads/payload-types.ts | 41 +++++++++ test/uploads/shared.ts | 1 + 18 files changed, 329 insertions(+), 27 deletions(-) create mode 100644 packages/ui/src/providers/UploadControls/index.tsx create mode 100644 test/uploads/collections/AdminUploadControl/components/UploadControl/index.client.tsx create mode 100644 test/uploads/collections/AdminUploadControl/components/UploadControl/index.tsx create mode 100644 test/uploads/collections/AdminUploadControl/index.ts diff --git a/packages/next/src/views/Document/renderDocumentSlots.tsx b/packages/next/src/views/Document/renderDocumentSlots.tsx index 7cf995a44..0eac0b7f1 100644 --- a/packages/next/src/views/Document/renderDocumentSlots.tsx +++ b/packages/next/src/views/Document/renderDocumentSlots.tsx @@ -157,6 +157,14 @@ export const renderDocumentSlots: (args: { }) } + if (collectionConfig?.upload && collectionConfig.upload.admin?.components?.controls) { + components.UploadControls = RenderServerComponent({ + Component: collectionConfig.upload.admin.components.controls, + importMap: req.payload.importMap, + serverProps, + }) + } + return components } diff --git a/packages/payload/src/admin/types.ts b/packages/payload/src/admin/types.ts index 653278660..42ebef54e 100644 --- a/packages/payload/src/admin/types.ts +++ b/packages/payload/src/admin/types.ts @@ -566,6 +566,7 @@ export type DocumentSlots = { SaveButton?: React.ReactNode SaveDraftButton?: React.ReactNode Upload?: React.ReactNode + UploadControls?: React.ReactNode } export type { diff --git a/packages/payload/src/bin/generateImportMap/iterateCollections.ts b/packages/payload/src/bin/generateImportMap/iterateCollections.ts index d7eae484d..303742637 100644 --- a/packages/payload/src/bin/generateImportMap/iterateCollections.ts +++ b/packages/payload/src/bin/generateImportMap/iterateCollections.ts @@ -44,6 +44,10 @@ export function iterateCollections({ addToImportMap(collection.admin?.components?.edit?.SaveDraftButton) addToImportMap(collection.admin?.components?.edit?.Upload) + if (collection.upload?.admin?.components?.controls) { + addToImportMap(collection.upload?.admin?.components?.controls) + } + if (collection.admin?.components?.views?.edit) { for (const editViewConfig of Object.values(collection.admin?.components?.views?.edit)) { if ('Component' in editViewConfig) { diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index da29c9dd0..17f7f78cb 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -1,6 +1,7 @@ import type { ResizeOptions, Sharp } from 'sharp' import type { TypeWithID } from '../collections/config/types.js' +import type { PayloadComponent } from '../config/types.js' import type { PayloadRequest } from '../types/index.js' import type { WithMetadata } from './optionallyAppendMetadata.js' @@ -101,12 +102,25 @@ export type AllowList = Array<{ search?: string }> +type Admin = { + components?: { + /** + * The Controls component to extend the upload controls in the admin panel. + */ + controls?: PayloadComponent[] + } +} + export type UploadConfig = { /** * The adapter name to use for uploads. Used for storage adapter telemetry. * @default undefined */ adapter?: string + /** + * The admin configuration for the upload field. + */ + admin?: Admin /** * Represents an admin thumbnail, which can be either a React component or a string. * - If a string, it should be one of the image size names. @@ -201,6 +215,7 @@ export type UploadConfig = { * @default undefined */ modifyResponseHeaders?: ({ headers }: { headers: Headers }) => Headers + /** * Controls the behavior of pasting/uploading files from URLs. * If set to `false`, fetching from remote URLs is disabled. diff --git a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx index 0fe87a64f..3a462bc32 100644 --- a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx +++ b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx @@ -22,8 +22,8 @@ import { DocumentFields } from '../../DocumentFields/index.js' import { MoveDocToFolder } from '../../FolderView/MoveDocToFolder/index.js' import { Upload_v4 } from '../../Upload/index.js' import { useFormsManager } from '../FormsManager/index.js' -import { BulkUploadProvider } from '../index.js' import './index.scss' +import { BulkUploadProvider } from '../index.js' const baseClass = 'collection-edit' diff --git a/packages/ui/src/elements/BulkUpload/index.tsx b/packages/ui/src/elements/BulkUpload/index.tsx index cba2916d5..3bd36075e 100644 --- a/packages/ui/src/elements/BulkUpload/index.tsx +++ b/packages/ui/src/elements/BulkUpload/index.tsx @@ -9,6 +9,7 @@ import { toast } from 'sonner' import { useConfig } from '../../providers/Config/index.js' import { useTranslation } from '../../providers/Translation/index.js' +import { UploadControlsProvider } from '../../providers/UploadControls/index.js' import { Drawer, useDrawerDepth } from '../Drawer/index.js' import { AddFilesView } from './AddFilesView/index.js' import { AddingFilesView } from './AddingFilesView/index.js' @@ -75,7 +76,9 @@ export function BulkUploadDrawer() { return ( - + + + ) diff --git a/packages/ui/src/elements/Upload/index.scss b/packages/ui/src/elements/Upload/index.scss index 1a3ec5ce9..375f7567e 100644 --- a/packages/ui/src/elements/Upload/index.scss +++ b/packages/ui/src/elements/Upload/index.scss @@ -90,6 +90,7 @@ &__dropzoneButtons { display: flex; gap: calc(var(--base) * 0.5); + align-items: center; } &__orText { diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index 60c93bc6f..299de3c3a 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -14,15 +14,16 @@ import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { EditDepthProvider } from '../../providers/EditDepth/index.js' import { useTranslation } from '../../providers/Translation/index.js' +import { UploadControlsProvider, useUploadControls } from '../../providers/UploadControls/index.js' import { useUploadEdits } from '../../providers/UploadEdits/index.js' import { Button } from '../Button/index.js' import { Drawer } from '../Drawer/index.js' import { Dropzone } from '../Dropzone/index.js' import { EditUpload } from '../EditUpload/index.js' +import './index.scss' import { FileDetails } from '../FileDetails/index.js' import { PreviewSizes } from '../PreviewSizes/index.js' import { Thumbnail } from '../Thumbnail/index.js' -import './index.scss' const baseClass = 'file-field' export const editDrawerSlug = 'edit-upload' @@ -106,17 +107,20 @@ export type UploadProps = { readonly initialState?: FormState readonly onChange?: (file?: File) => void readonly uploadConfig: SanitizedCollectionConfig['upload'] + readonly UploadControls?: React.ReactNode } export const Upload: React.FC = (props) => { const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits() return ( - + + + ) } @@ -135,9 +139,19 @@ export const Upload_v4: React.FC = (props) => { resetUploadEdits, updateUploadEdits, uploadConfig, + UploadControls, uploadEdits, } = props + const { + setUploadControlFile, + setUploadControlFileName, + setUploadControlFileUrl, + uploadControlFile, + uploadControlFileName, + uploadControlFileUrl, + } = useUploadControls() + const { config: { routes: { api }, @@ -174,12 +188,15 @@ export const Upload_v4: React.FC = (props) => { setValue(newFile) setShowUrlInput(false) + setUploadControlFileUrl('') + setUploadControlFileName(null) + setUploadControlFile(null) if (typeof onChange === 'function') { onChange(newFile) } }, - [onChange, setValue], + [onChange, setValue, setUploadControlFile, setUploadControlFileName, setUploadControlFileUrl], ) const renameFile = (fileToChange: File, newName: string): File => { @@ -218,7 +235,16 @@ export const Upload_v4: React.FC = (props) => { setFileUrl('') resetUploadEdits() setShowUrlInput(false) - }, [handleFileChange, resetUploadEdits]) + setUploadControlFileUrl('') + setUploadControlFileName(null) + setUploadControlFile(null) + }, [ + handleFileChange, + resetUploadEdits, + setUploadControlFile, + setUploadControlFileName, + setUploadControlFileUrl, + ]) const onEditsSave = useCallback( (args: UploadEdits) => { @@ -228,7 +254,7 @@ export const Upload_v4: React.FC = (props) => { [setModified, updateUploadEdits], ) - const handleUrlSubmit = async () => { + const handleUrlSubmit = useCallback(async () => { if (!fileUrl || uploadConfig?.pasteURL === false) { return } @@ -243,7 +269,7 @@ export const Upload_v4: React.FC = (props) => { } const blob = await clientResponse.blob() - const fileName = decodeURIComponent(fileUrl.split('/').pop() || '') + const fileName = uploadControlFileName || decodeURIComponent(fileUrl.split('/').pop() || '') const file = new File([blob], fileName, { type: blob.type }) handleFileChange(file) @@ -277,7 +303,17 @@ export const Upload_v4: React.FC = (props) => { toast.error('The provided URL is not allowed.') setUploadStatus('failed') } - } + }, [ + fileUrl, + uploadConfig, + setUploadStatus, + handleFileChange, + useServerSideFetch, + collectionSlug, + id, + serverURL, + api, + ]) useEffect(() => { if (initialState?.file?.value instanceof File) { @@ -314,9 +350,26 @@ export const Upload_v4: React.FC = (props) => { const imageCacheTag = uploadConfig?.cacheTags && savedDocumentData?.updatedAt - if (uploadConfig.hideFileInputOnCreate && !savedDocumentData?.filename) { - return null - } + useEffect(() => { + const handleControlFileUrl = async () => { + if (uploadControlFileUrl) { + setFileUrl(uploadControlFileUrl) + await handleUrlSubmit() + } + } + + void handleControlFileUrl() + }, [uploadControlFileUrl, handleUrlSubmit]) + + useEffect(() => { + const handleControlFile = () => { + if (uploadControlFile) { + handleFileChange(uploadControlFile) + } + } + + void handleControlFile() + }, [uploadControlFile, handleFileChange]) return (
@@ -371,6 +424,9 @@ export const Upload_v4: React.FC = (props) => { buttonStyle="pill" onClick={() => { setShowUrlInput(true) + setUploadControlFileUrl('') + setUploadControlFile(null) + setUploadControlFileName(null) }} size="small" > @@ -378,8 +434,9 @@ export const Upload_v4: React.FC = (props) => { )} -
+ {UploadControls ? UploadControls : null} +

{t('general:or')} {t('upload:dragAndDrop')}

@@ -419,6 +476,9 @@ export const Upload_v4: React.FC = (props) => { iconStyle="with-border" onClick={() => { setShowUrlInput(false) + setUploadControlFileUrl('') + setUploadControlFile(null) + setUploadControlFileName(null) }} round tooltip={t('general:cancel')} diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 366609d15..da847eb64 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -296,6 +296,7 @@ export { DocumentEventsProvider, useDocumentEvents } from '../../providers/Docum export { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js' export { useDocumentTitle } from '../../providers/DocumentTitle/index.js' export type { DocumentInfoContext, DocumentInfoProps } from '../../providers/DocumentInfo/index.js' +export { useUploadControls } from '../../providers/UploadControls/index.js' export { EditDepthProvider, useEditDepth } from '../../providers/EditDepth/index.js' export { EntityVisibilityProvider, diff --git a/packages/ui/src/providers/UploadControls/index.tsx b/packages/ui/src/providers/UploadControls/index.tsx new file mode 100644 index 000000000..e8f5d05b7 --- /dev/null +++ b/packages/ui/src/providers/UploadControls/index.tsx @@ -0,0 +1,42 @@ +'use client' +import React, { createContext, use, useState } from 'react' + +export type UploadControlsContext = { + setUploadControlFile: (file: File) => void + setUploadControlFileName: (name: string) => void + setUploadControlFileUrl: (url: string) => void + uploadControlFile: File | null + uploadControlFileName: null | string + uploadControlFileUrl: string +} + +const Context = createContext(undefined) + +export const UploadControlsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [uploadControlFileName, setUploadControlFileName] = useState(null) + const [uploadControlFileUrl, setUploadControlFileUrl] = useState('') + const [uploadControlFile, setUploadControlFile] = useState(null) + + return ( + + {children} + + ) +} + +export const useUploadControls = (): UploadControlsContext => { + const context = use(Context) + if (!context) { + throw new Error('useUploadControls must be used within an UploadControlsProvider') + } + return context +} diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx index d4efb1ce1..373b982bc 100644 --- a/packages/ui/src/views/Edit/index.tsx +++ b/packages/ui/src/views/Edit/index.tsx @@ -27,6 +27,7 @@ import { useEditDepth } from '../../providers/EditDepth/index.js' import { OperationProvider } from '../../providers/Operation/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js' +import { UploadControlsProvider } from '../../providers/UploadControls/index.js' import { useUploadEdits } from '../../providers/UploadEdits/index.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js' @@ -34,8 +35,8 @@ import { handleGoBack } from '../../utilities/handleGoBack.js' import { handleTakeOver } from '../../utilities/handleTakeOver.js' import { Auth } from './Auth/index.js' import { SetDocumentStepNav } from './SetDocumentStepNav/index.js' -import { SetDocumentTitle } from './SetDocumentTitle/index.js' import './index.scss' +import { SetDocumentTitle } from './SetDocumentTitle/index.js' const baseClass = 'collection-edit' @@ -51,6 +52,7 @@ export function DefaultEditView({ SaveButton, SaveDraftButton, Upload: CustomUpload, + UploadControls, }: DocumentViewClientProps) { const { id, @@ -581,13 +583,16 @@ export function DefaultEditView({ )} {upload && ( - {CustomUpload || ( - - )} + + {CustomUpload || ( + + )} + )} diff --git a/test/uploads/collections/AdminUploadControl/components/UploadControl/index.client.tsx b/test/uploads/collections/AdminUploadControl/components/UploadControl/index.client.tsx new file mode 100644 index 000000000..56e3e8881 --- /dev/null +++ b/test/uploads/collections/AdminUploadControl/components/UploadControl/index.client.tsx @@ -0,0 +1,30 @@ +'use client' +import { Button, useUploadControls } from '@payloadcms/ui' +import React, { useCallback } from 'react' + +export const UploadControl = () => { + const { setUploadControlFile, setUploadControlFileUrl } = useUploadControls() + + const loadFromFile = useCallback(async () => { + const response = await fetch('https://payloadcms.com/images/universal-truth.jpg') + const blob = await response.blob() + const file = new File([blob], 'universal-truth.jpg', { type: 'image/jpeg' }) + setUploadControlFile(file) + }, [setUploadControlFile]) + + const loadFromUrl = useCallback(() => { + setUploadControlFileUrl('https://payloadcms.com/images/universal-truth.jpg') + }, [setUploadControlFileUrl]) + + return ( +
+ +
+ +
+ ) +} diff --git a/test/uploads/collections/AdminUploadControl/components/UploadControl/index.tsx b/test/uploads/collections/AdminUploadControl/components/UploadControl/index.tsx new file mode 100644 index 000000000..4504ff7c6 --- /dev/null +++ b/test/uploads/collections/AdminUploadControl/components/UploadControl/index.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +import { UploadControl } from './index.client.js' + +export const UploadControlRSC: React.FC = () => { + return ( +
+ +
+ ) +} diff --git a/test/uploads/collections/AdminUploadControl/index.ts b/test/uploads/collections/AdminUploadControl/index.ts new file mode 100644 index 000000000..9195082df --- /dev/null +++ b/test/uploads/collections/AdminUploadControl/index.ts @@ -0,0 +1,23 @@ +import type { CollectionConfig } from 'payload' + +import path from 'path' +import { fileURLToPath } from 'url' + +import { adminUploadControlSlug } from '../../shared.js' +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export const AdminUploadControl: CollectionConfig = { + slug: adminUploadControlSlug, + upload: { + staticDir: path.resolve(dirname, 'test/uploads/media'), + admin: { + components: { + controls: [ + '/collections/AdminUploadControl/components/UploadControl/index.js#UploadControlRSC', + ], + }, + }, + }, + fields: [], +} diff --git a/test/uploads/config.ts b/test/uploads/config.ts index 2751266e3..b0ad5b57e 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -12,6 +12,7 @@ import removeFiles from '../helpers/removeFiles.js' import { AdminThumbnailFunction } from './collections/AdminThumbnailFunction/index.js' import { AdminThumbnailSize } from './collections/AdminThumbnailSize/index.js' import { AdminThumbnailWithSearchQueries } from './collections/AdminThumbnailWithSearchQueries/index.js' +import { AdminUploadControl } from './collections/AdminUploadControl/index.js' import { CustomUploadFieldCollection } from './collections/CustomUploadField/index.js' import { Uploads1 } from './collections/Upload1/index.js' import { Uploads2 } from './collections/Upload2/index.js' @@ -639,6 +640,7 @@ export default buildConfigWithDefaults({ AdminThumbnailFunction, AdminThumbnailWithSearchQueries, AdminThumbnailSize, + AdminUploadControl, { slug: 'optional-file', fields: [], diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index ebdb479fa..5aaeac935 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -20,11 +20,12 @@ import { assertToastErrors } from '../helpers/assertToastErrors.js' import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js' import { reInitializeDB } from '../helpers/reInitializeDB.js' import { RESTClient } from '../helpers/rest.js' -import { TEST_TIMEOUT_LONG } from '../playwright.config.js' +import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { adminThumbnailFunctionSlug, adminThumbnailSizeSlug, adminThumbnailWithSearchQueries, + adminUploadControlSlug, animatedTypeMedia, audioSlug, customFileNameMediaSlug, @@ -55,6 +56,7 @@ let mediaURL: AdminUrlUtil let animatedTypeMediaURL: AdminUrlUtil let audioURL: AdminUrlUtil let relationURL: AdminUrlUtil +let adminUploadControlURL: AdminUrlUtil let adminThumbnailSizeURL: AdminUrlUtil let adminThumbnailFunctionURL: AdminUrlUtil let adminThumbnailWithSearchQueriesURL: AdminUrlUtil @@ -89,6 +91,7 @@ describe('Uploads', () => { animatedTypeMediaURL = new AdminUrlUtil(serverURL, animatedTypeMedia) audioURL = new AdminUrlUtil(serverURL, audioSlug) relationURL = new AdminUrlUtil(serverURL, relationSlug) + adminUploadControlURL = new AdminUrlUtil(serverURL, adminUploadControlSlug) adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug) adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug) adminThumbnailWithSearchQueriesURL = new AdminUrlUtil( @@ -520,6 +523,57 @@ describe('Uploads', () => { ) }) + test('should render adminUploadControls', async () => { + await page.goto(adminUploadControlURL.create) + + const loadFromFileButton = page.locator('#load-from-file-upload-button') + const loadFromUrlButton = page.locator('#load-from-url-upload-button') + await expect(loadFromFileButton).toBeVisible() + await expect(loadFromUrlButton).toBeVisible() + }) + + test('should load a file using a file reference from custom controls', async () => { + await page.goto(adminUploadControlURL.create) + + const loadFromFileButton = page.locator('#load-from-file-upload-button') + await loadFromFileButton.click() + await wait(1000) + + await page.locator('#action-save').click() + await expect(page.locator('.payload-toast-container')).toContainText('successfully') + await wait(1000) + + const mediaID = page.url().split('/').pop() + const { doc: mediaDoc } = await client.findByID({ + id: mediaID as string, + slug: adminUploadControlSlug, + auth: true, + }) + await expect + .poll(() => mediaDoc.filename, { timeout: POLL_TOPASS_TIMEOUT }) + .toContain('universal-truth') + }) + + test('should load a file using a URL reference from custom controls', async () => { + await page.goto(adminUploadControlURL.create) + + const loadFromUrlButton = page.locator('#load-from-url-upload-button') + await loadFromUrlButton.click() + await page.locator('#action-save').click() + await expect(page.locator('.payload-toast-container')).toContainText('successfully') + await wait(1000) + + const mediaID = page.url().split('/').pop() + const { doc: mediaDoc } = await client.findByID({ + id: mediaID as string, + slug: adminUploadControlSlug, + auth: true, + }) + await expect + .poll(() => mediaDoc.filename, { timeout: POLL_TOPASS_TIMEOUT }) + .toContain('universal-truth') + }) + test('should render adminThumbnail when using a function', async () => { await page.goto(adminThumbnailFunctionURL.list) diff --git a/test/uploads/payload-types.ts b/test/uploads/payload-types.ts index bf730324c..d2ec1ab02 100644 --- a/test/uploads/payload-types.ts +++ b/test/uploads/payload-types.ts @@ -94,6 +94,7 @@ export interface Config { 'admin-thumbnail-function': AdminThumbnailFunction; 'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQuery; 'admin-thumbnail-size': AdminThumbnailSize; + 'admin-upload-control': AdminUploadControl; 'optional-file': OptionalFile; 'required-file': RequiredFile; versions: Version; @@ -140,6 +141,7 @@ export interface Config { 'admin-thumbnail-function': AdminThumbnailFunctionSelect | AdminThumbnailFunctionSelect; 'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQueriesSelect | AdminThumbnailWithSearchQueriesSelect; 'admin-thumbnail-size': AdminThumbnailSizeSelect | AdminThumbnailSizeSelect; + 'admin-upload-control': AdminUploadControlSelect | AdminUploadControlSelect; 'optional-file': OptionalFileSelect | OptionalFileSelect; 'required-file': RequiredFileSelect | RequiredFileSelect; versions: VersionsSelect | VersionsSelect; @@ -1179,6 +1181,24 @@ export interface AdminThumbnailWithSearchQuery { focalX?: number | null; focalY?: number | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "admin-upload-control". + */ +export interface AdminUploadControl { + id: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "optional-file". @@ -1479,6 +1499,10 @@ export interface PayloadLockedDocument { relationTo: 'admin-thumbnail-size'; value: string | AdminThumbnailSize; } | null) + | ({ + relationTo: 'admin-upload-control'; + value: string | AdminUploadControl; + } | null) | ({ relationTo: 'optional-file'; value: string | OptionalFile; @@ -2616,6 +2640,23 @@ export interface AdminThumbnailSizeSelect { }; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "admin-upload-control_select". + */ +export interface AdminUploadControlSelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "optional-file_select". diff --git a/test/uploads/shared.ts b/test/uploads/shared.ts index b271c7508..bdcc50e47 100644 --- a/test/uploads/shared.ts +++ b/test/uploads/shared.ts @@ -11,6 +11,7 @@ export const relationPreviewSlug = 'relation-preview' export const mediaWithRelationPreviewSlug = 'media-with-relation-preview' export const mediaWithoutRelationPreviewSlug = 'media-without-relation-preview' export const mediaWithoutCacheTagsSlug = 'media-without-cache-tags' +export const adminUploadControlSlug = 'admin-upload-control' export const adminThumbnailFunctionSlug = 'admin-thumbnail-function' export const adminThumbnailWithSearchQueries = 'admin-thumbnail-with-search-queries' export const adminThumbnailSizeSlug = 'admin-thumbnail-size'