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:
@@ -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