fix(ui): ensure file field is only serialized at top-level for upload-enabled collections (#12074)

This fixes an issue where fields with the name `file` was being
serialized as a top-level field in multipart form data even when the
collection was not upload-enabled. This caused the value of `file` (when
used as a regular field like a text, array, etc.) to be stripped from
the `_payload`.

- Updated `createFormData` to only delete `data.file` and serialize it
at the top level if `docConfig.upload` is defined.
- This prevents unintended loss of `file` field values for non-upload
collections.

The `file` field now remains safely nested in `_payload` unless it's
part of an upload-enabled collection.
This commit is contained in:
Patrik
2025-04-10 13:37:10 -04:00
committed by GitHub
parent eab9770315
commit 112e081d8f
4 changed files with 49 additions and 25 deletions

View File

@@ -56,7 +56,8 @@ import { initContextState } from './initContextState.js'
const baseClass = 'form' const baseClass = 'form'
export const Form: React.FC<FormProps> = (props) => { export const Form: React.FC<FormProps> = (props) => {
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo() const { id, collectionSlug, docConfig, docPermissions, getDocPreferences, globalSlug } =
useDocumentInfo()
const { const {
action, action,
@@ -491,8 +492,28 @@ export const Form: React.FC<FormProps> = (props) => {
let file = data?.file let file = data?.file
if (file) { if (docConfig && 'upload' in docConfig && docConfig.upload && file) {
delete data.file delete data.file
const handler = getUploadHandler({ collectionSlug })
if (typeof handler === 'function') {
let filename = file.name
const clientUploadContext = await handler({
file,
updateFilename: (value) => {
filename = value
},
})
file = JSON.stringify({
clientUploadContext,
collectionSlug,
filename,
mimeType: file.type,
size: file.size,
})
}
} }
if (mergeOverrideData) { if (mergeOverrideData) {
@@ -504,37 +525,23 @@ export const Form: React.FC<FormProps> = (props) => {
data = overrides data = overrides
} }
const handler = getUploadHandler({ collectionSlug }) const dataToSerialize: Record<string, unknown> = {
_payload: JSON.stringify(data),
if (file && typeof handler === 'function') {
let filename = file.name
const clientUploadContext = await handler({
file,
updateFilename: (value) => {
filename = value
},
})
file = JSON.stringify({
clientUploadContext,
collectionSlug,
filename,
mimeType: file.type,
size: file.size,
})
} }
const dataToSerialize = { if (docConfig && 'upload' in docConfig && docConfig.upload && file) {
_payload: JSON.stringify(data), dataToSerialize.file = file
file,
} }
// nullAsUndefineds is important to allow uploads and relationship fields to clear themselves // nullAsUndefineds is important to allow uploads and relationship fields to clear themselves
const formData = serialize(dataToSerialize, { indices: true, nullsAsUndefineds: false }) const formData = serialize(dataToSerialize, {
indices: true,
nullsAsUndefineds: false,
})
return formData return formData
}, },
[collectionSlug, getUploadHandler], [collectionSlug, docConfig, getUploadHandler],
) )
const reset = useCallback( const reset = useCallback(

View File

@@ -236,6 +236,10 @@ export const Posts: CollectionConfig = {
}, },
], ],
}, },
{
name: 'file',
type: 'text',
},
], ],
labels: { labels: {
plural: slugPluralLabel, plural: slugPluralLabel,

View File

@@ -512,6 +512,17 @@ describe('Document View', () => {
await expect(publishButton).toContainText('Publish in English') await expect(publishButton).toContainText('Publish in English')
}) })
}) })
describe('reserved field names', () => {
test('should allow creation of field named file in non-upload enabled collection', async () => {
await page.goto(postsUrl.create)
const fileField = page.locator('#field-file')
await fileField.fill('some file text')
await saveDocAndAssert(page)
await expect(fileField).toHaveValue('some file text')
})
})
}) })
async function createPost(overrides?: Partial<Post>): Promise<Post> { async function createPost(overrides?: Partial<Post>): Promise<Post> {

View File

@@ -247,6 +247,7 @@ export interface Post {
sidebarField?: string | null; sidebarField?: string | null;
wavelengths?: ('fm' | 'am') | null; wavelengths?: ('fm' | 'am') | null;
selectField?: ('option1' | 'option2')[] | null; selectField?: ('option1' | 'option2')[] | null;
file?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
_status?: ('draft' | 'published') | null; _status?: ('draft' | 'published') | null;
@@ -665,6 +666,7 @@ export interface PostsSelect<T extends boolean = true> {
sidebarField?: T; sidebarField?: T;
wavelengths?: T; wavelengths?: T;
selectField?: T; selectField?: T;
file?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
_status?: T; _status?: T;