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:
Jessica Rynkar
2025-06-16 19:56:05 +01:00
committed by GitHub
parent 769ca03bff
commit b372a34ebf
7 changed files with 80 additions and 3 deletions

View File

@@ -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) |
| **`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. |
| **`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) |
| **`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). |

View File

@@ -67,6 +67,7 @@ export const generateFileData = async <T>({
})
const {
constructorOptions = {},
disableLocalStorage,
focalPoint: focalPointEnabled = true,
formatOptions,
@@ -143,9 +144,11 @@ export const generateFileData = async <T>({
let mime: string
const fileHasAdjustments =
fileSupportsResize &&
Boolean(resizeOptions || formatOptions || trimOptions || file.tempFilePath)
Boolean(
resizeOptions || formatOptions || trimOptions || constructorOptions || file.tempFilePath,
)
const sharpOptions: SharpOptions = {}
const sharpOptions: SharpOptions = { ...constructorOptions }
if (fileIsAnimatedType) {
sharpOptions.animated = true

View File

@@ -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 { PayloadComponent } from '../config/types.js'
@@ -138,6 +138,11 @@ export type UploadConfig = {
* @default true
*/
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.
* @default true
@@ -186,6 +191,7 @@ export type UploadConfig = {
* - If a handler returns null, the next handler will be run.
* - If no handlers return a response the file will be returned by default.
*
* @link https://sharp.pixelplumbing.com/api-output/#toformat
* @default undefined
*/
handlers?: ((

View File

@@ -20,6 +20,7 @@ import {
allowListMediaSlug,
animatedTypeMedia,
audioSlug,
constructorOptionsSlug,
customFileNameMediaSlug,
enlargeSlug,
focalNoSizesSlug,
@@ -832,6 +833,16 @@ export default buildConfigWithDefaults({
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) => {
const uploadsDir = path.resolve(dirname, './media')

View File

@@ -28,6 +28,7 @@ import {
adminUploadControlSlug,
animatedTypeMedia,
audioSlug,
constructorOptionsSlug,
customFileNameMediaSlug,
customUploadFieldSlug,
focalOnlySlug,
@@ -75,6 +76,7 @@ let hideFileInputOnCreateURL: AdminUrlUtil
let bestFitURL: AdminUrlUtil
let withoutEnlargementResizeOptionsURL: AdminUrlUtil
let threeDimensionalURL: AdminUrlUtil
let constructorOptionsURL: AdminUrlUtil
let consoleErrorsFromPage: string[] = []
let collectErrorsFromPage: () => boolean
let stopCollectingErrorsFromPage: () => boolean
@@ -113,6 +115,7 @@ describe('Uploads', () => {
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
withoutEnlargementResizeOptionsURL = new AdminUrlUtil(serverURL, withoutEnlargeSlug)
threeDimensionalURL = new AdminUrlUtil(serverURL, threeDimensionalSlug)
constructorOptionsURL = new AdminUrlUtil(serverURL, constructorOptionsSlug)
const context = await browser.newContext()
page = await context.newPage()
@@ -1531,4 +1534,15 @@ describe('Uploads', () => {
await expect(imageUploadCell).toHaveText('<No Image Upload>')
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')
})
})

View File

@@ -107,6 +107,7 @@ export interface Config {
'best-fit': BestFit;
'list-view-preview': ListViewPreview;
'three-dimensional': ThreeDimensional;
'constructor-options': ConstructorOption;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -154,6 +155,7 @@ export interface Config {
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
'list-view-preview': ListViewPreviewSelect<false> | ListViewPreviewSelect<true>;
'three-dimensional': ThreeDimensionalSelect<false> | ThreeDimensionalSelect<true>;
'constructor-options': ConstructorOptionsSelect<false> | ConstructorOptionsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -1367,6 +1369,24 @@ export interface ThreeDimensional {
width?: 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
* via the `definition` "users".
@@ -1551,6 +1571,10 @@ export interface PayloadLockedDocument {
relationTo: 'three-dimensional';
value: string | ThreeDimensional;
} | null)
| ({
relationTo: 'constructor-options';
value: string | ConstructorOption;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -2852,6 +2876,23 @@ export interface ThreeDimensionalSelect<T extends boolean = true> {
width?: 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
* via the `definition` "users_select".

View File

@@ -28,3 +28,4 @@ export const allowListMediaSlug = 'allow-list-media'
export const listViewPreviewSlug = 'list-view-preview'
export const threeDimensionalSlug = 'three-dimensional'
export const constructorOptionsSlug = 'constructor-options'