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: [],
}
```

### 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'