feat(ui): adds constructorOptions to upload config (#12766)
### What? Adds `constructorOptions` property to the upload config to allow any of [these options](https://sharp.pixelplumbing.com/api-constructor/) to be passed to the Sharp library. ### Why? Users should be able to extend the Sharp library config as needed, to define useful properties like `limitInputPixels` etc. ### How? Creates new config option `constructorOptions` which passes any compatible options directly to the Sharp library. #### Reported by client.
This commit is contained in:
@@ -95,6 +95,7 @@ _An asterisk denotes that an option is required._
|
|||||||
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
|
||||||
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
|
| **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true |
|
||||||
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
|
| **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. |
|
||||||
|
| **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) |
|
||||||
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
|
||||||
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
|
||||||
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
|
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const generateFileData = async <T>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
constructorOptions = {},
|
||||||
disableLocalStorage,
|
disableLocalStorage,
|
||||||
focalPoint: focalPointEnabled = true,
|
focalPoint: focalPointEnabled = true,
|
||||||
formatOptions,
|
formatOptions,
|
||||||
@@ -143,9 +144,11 @@ export const generateFileData = async <T>({
|
|||||||
let mime: string
|
let mime: string
|
||||||
const fileHasAdjustments =
|
const fileHasAdjustments =
|
||||||
fileSupportsResize &&
|
fileSupportsResize &&
|
||||||
Boolean(resizeOptions || formatOptions || trimOptions || file.tempFilePath)
|
Boolean(
|
||||||
|
resizeOptions || formatOptions || trimOptions || constructorOptions || file.tempFilePath,
|
||||||
|
)
|
||||||
|
|
||||||
const sharpOptions: SharpOptions = {}
|
const sharpOptions: SharpOptions = { ...constructorOptions }
|
||||||
|
|
||||||
if (fileIsAnimatedType) {
|
if (fileIsAnimatedType) {
|
||||||
sharpOptions.animated = true
|
sharpOptions.animated = true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ResizeOptions, Sharp } from 'sharp'
|
import type { ResizeOptions, Sharp, SharpOptions } from 'sharp'
|
||||||
|
|
||||||
import type { TypeWithID } from '../collections/config/types.js'
|
import type { TypeWithID } from '../collections/config/types.js'
|
||||||
import type { PayloadComponent } from '../config/types.js'
|
import type { PayloadComponent } from '../config/types.js'
|
||||||
@@ -138,6 +138,11 @@ export type UploadConfig = {
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
cacheTags?: boolean
|
cacheTags?: boolean
|
||||||
|
/**
|
||||||
|
* Sharp constructor options to be passed to the uploaded file.
|
||||||
|
* @link https://sharp.pixelplumbing.com/api-constructor/#sharp
|
||||||
|
*/
|
||||||
|
constructorOptions?: SharpOptions
|
||||||
/**
|
/**
|
||||||
* Enables cropping of images.
|
* Enables cropping of images.
|
||||||
* @default true
|
* @default true
|
||||||
@@ -186,6 +191,7 @@ export type UploadConfig = {
|
|||||||
* - If a handler returns null, the next handler will be run.
|
* - If a handler returns null, the next handler will be run.
|
||||||
* - If no handlers return a response the file will be returned by default.
|
* - If no handlers return a response the file will be returned by default.
|
||||||
*
|
*
|
||||||
|
* @link https://sharp.pixelplumbing.com/api-output/#toformat
|
||||||
* @default undefined
|
* @default undefined
|
||||||
*/
|
*/
|
||||||
handlers?: ((
|
handlers?: ((
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
allowListMediaSlug,
|
allowListMediaSlug,
|
||||||
animatedTypeMedia,
|
animatedTypeMedia,
|
||||||
audioSlug,
|
audioSlug,
|
||||||
|
constructorOptionsSlug,
|
||||||
customFileNameMediaSlug,
|
customFileNameMediaSlug,
|
||||||
enlargeSlug,
|
enlargeSlug,
|
||||||
focalNoSizesSlug,
|
focalNoSizesSlug,
|
||||||
@@ -832,6 +833,16 @@ export default buildConfigWithDefaults({
|
|||||||
focalPoint: false,
|
focalPoint: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: constructorOptionsSlug,
|
||||||
|
fields: [],
|
||||||
|
upload: {
|
||||||
|
constructorOptions: {
|
||||||
|
limitInputPixels: 100, // set lower than the collection upload fileSize limit default to test
|
||||||
|
},
|
||||||
|
staticDir: path.resolve(dirname, './media'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
const uploadsDir = path.resolve(dirname, './media')
|
const uploadsDir = path.resolve(dirname, './media')
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
adminUploadControlSlug,
|
adminUploadControlSlug,
|
||||||
animatedTypeMedia,
|
animatedTypeMedia,
|
||||||
audioSlug,
|
audioSlug,
|
||||||
|
constructorOptionsSlug,
|
||||||
customFileNameMediaSlug,
|
customFileNameMediaSlug,
|
||||||
customUploadFieldSlug,
|
customUploadFieldSlug,
|
||||||
focalOnlySlug,
|
focalOnlySlug,
|
||||||
@@ -75,6 +76,7 @@ let hideFileInputOnCreateURL: AdminUrlUtil
|
|||||||
let bestFitURL: AdminUrlUtil
|
let bestFitURL: AdminUrlUtil
|
||||||
let withoutEnlargementResizeOptionsURL: AdminUrlUtil
|
let withoutEnlargementResizeOptionsURL: AdminUrlUtil
|
||||||
let threeDimensionalURL: AdminUrlUtil
|
let threeDimensionalURL: AdminUrlUtil
|
||||||
|
let constructorOptionsURL: AdminUrlUtil
|
||||||
let consoleErrorsFromPage: string[] = []
|
let consoleErrorsFromPage: string[] = []
|
||||||
let collectErrorsFromPage: () => boolean
|
let collectErrorsFromPage: () => boolean
|
||||||
let stopCollectingErrorsFromPage: () => boolean
|
let stopCollectingErrorsFromPage: () => boolean
|
||||||
@@ -113,6 +115,7 @@ describe('Uploads', () => {
|
|||||||
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
|
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
|
||||||
withoutEnlargementResizeOptionsURL = new AdminUrlUtil(serverURL, withoutEnlargeSlug)
|
withoutEnlargementResizeOptionsURL = new AdminUrlUtil(serverURL, withoutEnlargeSlug)
|
||||||
threeDimensionalURL = new AdminUrlUtil(serverURL, threeDimensionalSlug)
|
threeDimensionalURL = new AdminUrlUtil(serverURL, threeDimensionalSlug)
|
||||||
|
constructorOptionsURL = new AdminUrlUtil(serverURL, constructorOptionsSlug)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -1531,4 +1534,15 @@ describe('Uploads', () => {
|
|||||||
await expect(imageUploadCell).toHaveText('<No Image Upload>')
|
await expect(imageUploadCell).toHaveText('<No Image Upload>')
|
||||||
await expect(imageRelationshipCell).toHaveText('<No Image Relationship>')
|
await expect(imageRelationshipCell).toHaveText('<No Image Relationship>')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should respect Sharp constructorOptions', async () => {
|
||||||
|
await page.goto(constructorOptionsURL.create)
|
||||||
|
|
||||||
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './animated.webp'))
|
||||||
|
|
||||||
|
const filename = page.locator('.file-field__filename')
|
||||||
|
|
||||||
|
await expect(filename).toHaveValue('animated.webp')
|
||||||
|
await saveDocAndAssert(page, '#action-save', 'error')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export interface Config {
|
|||||||
'best-fit': BestFit;
|
'best-fit': BestFit;
|
||||||
'list-view-preview': ListViewPreview;
|
'list-view-preview': ListViewPreview;
|
||||||
'three-dimensional': ThreeDimensional;
|
'three-dimensional': ThreeDimensional;
|
||||||
|
'constructor-options': ConstructorOption;
|
||||||
users: User;
|
users: User;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
@@ -154,6 +155,7 @@ export interface Config {
|
|||||||
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
|
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
|
||||||
'list-view-preview': ListViewPreviewSelect<false> | ListViewPreviewSelect<true>;
|
'list-view-preview': ListViewPreviewSelect<false> | ListViewPreviewSelect<true>;
|
||||||
'three-dimensional': ThreeDimensionalSelect<false> | ThreeDimensionalSelect<true>;
|
'three-dimensional': ThreeDimensionalSelect<false> | ThreeDimensionalSelect<true>;
|
||||||
|
'constructor-options': ConstructorOptionsSelect<false> | ConstructorOptionsSelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
@@ -1367,6 +1369,24 @@ export interface ThreeDimensional {
|
|||||||
width?: number | null;
|
width?: number | null;
|
||||||
height?: number | null;
|
height?: number | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "constructor-options".
|
||||||
|
*/
|
||||||
|
export interface ConstructorOption {
|
||||||
|
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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
@@ -1551,6 +1571,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'three-dimensional';
|
relationTo: 'three-dimensional';
|
||||||
value: string | ThreeDimensional;
|
value: string | ThreeDimensional;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'constructor-options';
|
||||||
|
value: string | ConstructorOption;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: string | User;
|
||||||
@@ -2852,6 +2876,23 @@ export interface ThreeDimensionalSelect<T extends boolean = true> {
|
|||||||
width?: T;
|
width?: T;
|
||||||
height?: T;
|
height?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "constructor-options_select".
|
||||||
|
*/
|
||||||
|
export interface ConstructorOptionsSelect<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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users_select".
|
* via the `definition` "users_select".
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ export const allowListMediaSlug = 'allow-list-media'
|
|||||||
|
|
||||||
export const listViewPreviewSlug = 'list-view-preview'
|
export const listViewPreviewSlug = 'list-view-preview'
|
||||||
export const threeDimensionalSlug = 'three-dimensional'
|
export const threeDimensionalSlug = 'three-dimensional'
|
||||||
|
export const constructorOptionsSlug = 'constructor-options'
|
||||||
|
|||||||
Reference in New Issue
Block a user