Skip to content

feat(ui): adds constructorOptions to upload config #12766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/upload/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
7 changes: 5 additions & 2 deletions packages/payload/src/uploads/generateFileData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const generateFileData = async <T>({
})

const {
constructorOptions = {},
disableLocalStorage,
focalPoint: focalPointEnabled = true,
formatOptions,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/payload/src/uploads/types.ts
Original file line number Diff line number Diff line change
@@ -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 { PayloadRequest } from '../types/index.js'
Expand Down Expand Up @@ -124,6 +124,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
Expand Down Expand Up @@ -172,6 +177,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?: ((
Expand Down
11 changes: 11 additions & 0 deletions test/uploads/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Uploads2 } from './collections/Upload2/index.js'
import {
animatedTypeMedia,
audioSlug,
constructorOptionsSlug,
customFileNameMediaSlug,
enlargeSlug,
focalNoSizesSlug,
Expand Down Expand Up @@ -808,6 +809,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')
Expand Down
14 changes: 14 additions & 0 deletions test/uploads/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
adminThumbnailWithSearchQueries,
animatedTypeMedia,
audioSlug,
constructorOptionsSlug,
customFileNameMediaSlug,
customUploadFieldSlug,
focalOnlySlug,
Expand Down Expand Up @@ -73,6 +74,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
Expand Down Expand Up @@ -110,6 +112,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()
Expand Down Expand Up @@ -1477,4 +1480,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')
})
})
41 changes: 41 additions & 0 deletions test/uploads/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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;
Expand Down Expand Up @@ -150,6 +151,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>;
Expand Down Expand Up @@ -1327,6 +1329,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".
Expand Down Expand Up @@ -1503,6 +1523,10 @@ export interface PayloadLockedDocument {
relationTo: 'three-dimensional';
value: string | ThreeDimensional;
} | null)
| ({
relationTo: 'constructor-options';
value: string | ConstructorOption;
} | null)
| ({
relationTo: 'users';
value: string | User;
Expand Down Expand Up @@ -2770,6 +2794,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".
Expand Down
1 change: 1 addition & 0 deletions test/uploads/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export const customFileNameMediaSlug = 'custom-file-name-media'

export const listViewPreviewSlug = 'list-view-preview'
export const threeDimensionalSlug = 'three-dimensional'
export const constructorOptionsSlug = 'constructor-options'
Loading