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 (
<div>
<Button id="load-from-file-upload-button" onClick={loadFromFile}>
Load from File
</Button>
<br />
<Button id="load-from-url-upload-button" onClick={loadFromUrl}>
Load from URL
</Button>
</div>
)
}
```
### Why?
Add the ability to use a custom component to select a document to
upload.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -566,6 +566,7 @@ export type DocumentSlots = {
|
||||
SaveButton?: React.ReactNode
|
||||
SaveDraftButton?: React.ReactNode
|
||||
Upload?: React.ReactNode
|
||||
UploadControls?: React.ReactNode
|
||||
}
|
||||
|
||||
export type {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Drawer gutter={false} Header={null} slug={drawerSlug}>
|
||||
<FormsManagerProvider>
|
||||
<UploadControlsProvider>
|
||||
<DrawerContent />
|
||||
</UploadControlsProvider>
|
||||
</FormsManagerProvider>
|
||||
</Drawer>
|
||||
)
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
&__dropzoneButtons {
|
||||
display: flex;
|
||||
gap: calc(var(--base) * 0.5);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__orText {
|
||||
|
||||
@@ -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<UploadProps> = (props) => {
|
||||
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||
return (
|
||||
<UploadControlsProvider>
|
||||
<Upload_v4
|
||||
{...props}
|
||||
resetUploadEdits={resetUploadEdits}
|
||||
updateUploadEdits={updateUploadEdits}
|
||||
uploadEdits={uploadEdits}
|
||||
/>
|
||||
</UploadControlsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -135,9 +139,19 @@ export const Upload_v4: React.FC<UploadProps_v4> = (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<UploadProps_v4> = (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<UploadProps_v4> = (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<UploadProps_v4> = (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<UploadProps_v4> = (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<UploadProps_v4> = (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<UploadProps_v4> = (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 (
|
||||
<div className={[fieldBaseClass, baseClass].filter(Boolean).join(' ')}>
|
||||
@@ -371,6 +424,9 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
buttonStyle="pill"
|
||||
onClick={() => {
|
||||
setShowUrlInput(true)
|
||||
setUploadControlFileUrl('')
|
||||
setUploadControlFile(null)
|
||||
setUploadControlFileName(null)
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
@@ -378,8 +434,9 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
</Button>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{UploadControls ? UploadControls : null}
|
||||
</div>
|
||||
<p className={`${baseClass}__dragAndDropText`}>
|
||||
{t('general:or')} {t('upload:dragAndDrop')}
|
||||
</p>
|
||||
@@ -419,6 +476,9 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
iconStyle="with-border"
|
||||
onClick={() => {
|
||||
setShowUrlInput(false)
|
||||
setUploadControlFileUrl('')
|
||||
setUploadControlFile(null)
|
||||
setUploadControlFileName(null)
|
||||
}}
|
||||
round
|
||||
tooltip={t('general:cancel')}
|
||||
|
||||
@@ -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,
|
||||
|
||||
42
packages/ui/src/providers/UploadControls/index.tsx
Normal file
42
packages/ui/src/providers/UploadControls/index.tsx
Normal file
@@ -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<UploadControlsContext>(undefined)
|
||||
|
||||
export const UploadControlsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [uploadControlFileName, setUploadControlFileName] = useState<null | string>(null)
|
||||
const [uploadControlFileUrl, setUploadControlFileUrl] = useState<string>('')
|
||||
const [uploadControlFile, setUploadControlFile] = useState<File | null>(null)
|
||||
|
||||
return (
|
||||
<Context
|
||||
value={{
|
||||
setUploadControlFile,
|
||||
setUploadControlFileName,
|
||||
setUploadControlFileUrl,
|
||||
uploadControlFile,
|
||||
uploadControlFileName,
|
||||
uploadControlFileUrl,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Context>
|
||||
)
|
||||
}
|
||||
|
||||
export const useUploadControls = (): UploadControlsContext => {
|
||||
const context = use(Context)
|
||||
if (!context) {
|
||||
throw new Error('useUploadControls must be used within an UploadControlsProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -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 && (
|
||||
<React.Fragment>
|
||||
<UploadControlsProvider>
|
||||
{CustomUpload || (
|
||||
<Upload
|
||||
collectionSlug={collectionConfig.slug}
|
||||
initialState={initialState}
|
||||
uploadConfig={upload}
|
||||
UploadControls={UploadControls}
|
||||
/>
|
||||
)}
|
||||
</UploadControlsProvider>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<Button id="load-from-file-upload-button" onClick={loadFromFile}>
|
||||
Load from File
|
||||
</Button>
|
||||
<br />
|
||||
<Button id="load-from-url-upload-button" onClick={loadFromUrl}>
|
||||
Load from URL
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import { UploadControl } from './index.client.js'
|
||||
|
||||
export const UploadControlRSC: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<UploadControl />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
test/uploads/collections/AdminUploadControl/index.ts
Normal file
23
test/uploads/collections/AdminUploadControl/index.ts
Normal file
@@ -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: [],
|
||||
}
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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<false> | AdminThumbnailFunctionSelect<true>;
|
||||
'admin-thumbnail-with-search-queries': AdminThumbnailWithSearchQueriesSelect<false> | AdminThumbnailWithSearchQueriesSelect<true>;
|
||||
'admin-thumbnail-size': AdminThumbnailSizeSelect<false> | AdminThumbnailSizeSelect<true>;
|
||||
'admin-upload-control': AdminUploadControlSelect<false> | AdminUploadControlSelect<true>;
|
||||
'optional-file': OptionalFileSelect<false> | OptionalFileSelect<true>;
|
||||
'required-file': RequiredFileSelect<false> | RequiredFileSelect<true>;
|
||||
versions: VersionsSelect<false> | VersionsSelect<true>;
|
||||
@@ -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<T extends boolean = true> {
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "admin-upload-control_select".
|
||||
*/
|
||||
export interface AdminUploadControlSelect<T extends boolean = true> {
|
||||
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".
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user