fix(ui): upload edits handling for bulk uploads (#12001)

### What?

This PR addresses a bug where image edits (crop, focal point, etc.) were
not persisting correctly in bulk uploads due to shared state logic with
single uploads.

### How?

- The `Upload` component now receives `uploadEdits`, `resetUploadEdits`,
and `updateUploadEdits` as props.
- `Upload_v4` was introduced to encapsulate the actual upload logic,
making it easier to reuse and test.
- The `AddingFilesView` and `EditForm` components are responsible for
injecting the correct `uploadEdits` state, depending on context.
- Avoided unnecessary `useFormsManager` usage in `Upload`.

Fixes #11868
This commit is contained in:
Patrik
2025-04-07 14:06:39 -04:00
committed by GitHub
parent 83319be752
commit c7b14bd44d
8 changed files with 125 additions and 17 deletions

View File

@@ -29,6 +29,8 @@ export function AddingFilesView() {
hasPublishPermission,
hasSavePermission,
hasSubmitted,
resetUploadEdits,
updateUploadEdits,
} = useFormsManager()
const activeForm = forms[activeIndex]
const { getEntityConfig } = useConfig()
@@ -67,7 +69,12 @@ export function AddingFilesView() {
versionCount={0}
>
<ActionsBar collectionConfig={collectionConfig} />
<EditForm submitted={hasSubmitted} />
<EditForm
resetUploadEdits={resetUploadEdits}
submitted={hasSubmitted}
updateUploadEdits={updateUploadEdits}
uploadEdits={activeForm?.uploadEdits}
/>
</DocumentInfoProvider>
) : null}
</div>

View File

@@ -16,11 +16,10 @@ import { useEditDepth } from '../../../providers/EditDepth/index.js'
import { OperationProvider } from '../../../providers/Operation/index.js'
import { useRouteTransition } from '../../../providers/RouteTransition/index.js'
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
import { useUploadEdits } from '../../../providers/UploadEdits/index.js'
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
import { DocumentFields } from '../../DocumentFields/index.js'
import { Upload } from '../../Upload/index.js'
import { Upload_v4 } from '../../Upload/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import { BulkUploadProvider } from '../index.js'
import './index.scss'
@@ -31,7 +30,12 @@ const baseClass = 'collection-edit'
// When rendered within a drawer, props are empty
// This is solely to support custom edit views which get server-rendered
export function EditForm({ submitted }: EditFormProps) {
export function EditForm({
resetUploadEdits,
submitted,
updateUploadEdits,
uploadEdits,
}: EditFormProps) {
const {
action,
collectionSlug: docSlug,
@@ -62,7 +66,6 @@ export function EditForm({ submitted }: EditFormProps) {
const depth = useEditDepth()
const params = useSearchParams()
const { reportUpdate } = useDocumentEvents()
const { resetUploadEdits } = useUploadEdits()
const { startRouteTransition } = useRouteTransition()
const locale = params.get('locale')
@@ -161,10 +164,13 @@ export function EditForm({ submitted }: EditFormProps) {
BeforeFields={
<React.Fragment>
{CustomUpload || (
<Upload
<Upload_v4
collectionSlug={collectionConfig.slug}
initialState={initialState}
resetUploadEdits={resetUploadEdits}
updateUploadEdits={updateUploadEdits}
uploadConfig={collectionConfig.upload}
uploadEdits={uploadEdits}
/>
)}
</React.Fragment>
@@ -185,7 +191,7 @@ function GetFieldProxy() {
const { getFields } = useForm()
const { getFormDataRef } = useFormsManager()
React.useEffect(() => {
useEffect(() => {
getFormDataRef.current = getFields
}, [getFields, getFormDataRef])

View File

@@ -1,3 +1,5 @@
import type { UploadProps_v4 } from '../../Upload/index.js'
export type EditFormProps = {
readonly submitted?: boolean
}
} & Pick<UploadProps_v4, 'resetUploadEdits' | 'updateUploadEdits' | 'uploadEdits'>

View File

@@ -1,6 +1,12 @@
'use client'
import type { Data, DocumentSlots, FormState, SanitizedDocumentPermissions } from 'payload'
import type {
Data,
DocumentSlots,
FormState,
SanitizedDocumentPermissions,
UploadEdits,
} from 'payload'
import { useModal } from '@faceless-ui/modal'
import { isImage } from 'payload/shared'
@@ -41,6 +47,7 @@ type FormsManagerContext = {
readonly hasSubmitted: boolean
readonly isInitializing: boolean
readonly removeFile: (index: number) => void
readonly resetUploadEdits?: () => void
readonly saveAllDocs: ({ overrides }?: { overrides?: Record<string, unknown> }) => Promise<void>
readonly setActiveIndex: (index: number) => void
readonly setFormTotalErrorCount: ({
@@ -52,6 +59,7 @@ type FormsManagerContext = {
}) => void
readonly thumbnailUrls: string[]
readonly totalErrorCount?: number
readonly updateUploadEdits: (args: UploadEdits) => void
}
const Context = React.createContext<FormsManagerContext>({
@@ -73,6 +81,7 @@ const Context = React.createContext<FormsManagerContext>({
setFormTotalErrorCount: () => {},
thumbnailUrls: [],
totalErrorCount: 0,
updateUploadEdits: () => {},
})
const initialState: State = {
@@ -242,6 +251,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
return {
errorCount: form.errorCount,
formState: currentFormsData,
uploadEdits: form.uploadEdits,
}
}
return form
@@ -295,6 +305,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
currentForms[activeIndex] = {
errorCount: currentForms[activeIndex].errorCount,
formState: currentFormsData,
uploadEdits: currentForms[activeIndex].uploadEdits,
}
const newDocs = []
@@ -306,7 +317,16 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
setLoadingText(t('general:uploadingBulk', { current: i + 1, total: currentForms.length }))
const req = await fetch(actionURL, {
const actionURLWithParams = `${actionURL}${qs.stringify(
{
uploadEdits: form?.uploadEdits || undefined,
},
{
addQueryPrefix: true,
},
)}`
const req = await fetch(actionURLWithParams, {
body: await createFormData(
form.formState,
overrides,
@@ -478,6 +498,31 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
[collectionSlug, docPermissions, forms, getFormState, hasSubmitted],
)
const updateUploadEdits = React.useCallback<FormsManagerContext['updateUploadEdits']>(
(uploadEdits) => {
dispatch({
type: 'UPDATE_FORM',
errorCount: forms[activeIndex].errorCount,
formState: forms[activeIndex].formState,
index: activeIndex,
uploadEdits,
})
},
[activeIndex, forms],
)
const resetUploadEdits = React.useCallback<FormsManagerContext['resetUploadEdits']>(() => {
dispatch({
type: 'REPLACE',
state: {
forms: forms.map((form) => ({
...form,
uploadEdits: {},
})),
},
})
}, [forms])
React.useEffect(() => {
if (!collectionSlug) {
return
@@ -529,11 +574,13 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
hasSubmitted,
isInitializing,
removeFile,
resetUploadEdits,
saveAllDocs,
setActiveIndex,
setFormTotalErrorCount,
thumbnailUrls: renderedThumbnails,
totalErrorCount,
updateUploadEdits,
}}
>
{isUploading && (

View File

@@ -1,10 +1,11 @@
import type { FormState } from 'payload'
import type { FormState, UploadEdits } from 'payload'
export type State = {
activeIndex: number
forms: {
errorCount: number
formState: FormState
uploadEdits?: UploadEdits
}[]
totalErrorCount: number
}
@@ -21,6 +22,7 @@ type Action =
index: number
type: 'UPDATE_FORM'
updatedFields?: Record<string, unknown>
uploadEdits?: UploadEdits
}
| {
files: FileList
@@ -55,6 +57,7 @@ export function formsManagementReducer(state: State, action: Action): State {
value: action.files[i],
},
},
uploadEdits: {},
}
}
@@ -117,6 +120,10 @@ export function formsManagementReducer(state: State, action: Action): State {
...updatedForms[action.index].formState,
...action.formState,
},
uploadEdits: {
...updatedForms[action.index].uploadEdits,
...action.uploadEdits,
},
}
return {

View File

@@ -19,9 +19,9 @@ import { Drawer, DrawerToggler } from '../Drawer/index.js'
import { Dropzone } from '../Dropzone/index.js'
import { EditUpload } from '../EditUpload/index.js'
import { FileDetails } from '../FileDetails/index.js'
import './index.scss'
import { PreviewSizes } from '../PreviewSizes/index.js'
import { Thumbnail } from '../Thumbnail/index.js'
import './index.scss'
const baseClass = 'file-field'
export const editDrawerSlug = 'edit-upload'
@@ -91,7 +91,34 @@ export type UploadProps = {
}
export const Upload: React.FC<UploadProps> = (props) => {
const { collectionSlug, customActions, initialState, onChange, uploadConfig } = props
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
return (
<Upload_v4
{...props}
resetUploadEdits={resetUploadEdits}
updateUploadEdits={updateUploadEdits}
uploadEdits={uploadEdits}
/>
)
}
export type UploadProps_v4 = {
readonly resetUploadEdits?: () => void
readonly updateUploadEdits?: (args: UploadEdits) => void
readonly uploadEdits?: UploadEdits
} & UploadProps
export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
const {
collectionSlug,
customActions,
initialState,
onChange,
resetUploadEdits,
updateUploadEdits,
uploadConfig,
uploadEdits,
} = props
const {
config: {
@@ -102,7 +129,6 @@ export const Upload: React.FC<UploadProps> = (props) => {
const { t } = useTranslation()
const { setModified } = useForm()
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
const { id, docPermissions, savedDocumentData, setUploadStatus } = useDocumentInfo()
const isFormSubmitting = useFormProcessing()
const { errorMessage, setValue, showError, value } = useField<File>({

View File

@@ -3,20 +3,26 @@ import type { UploadEdits } from 'payload'
import React from 'react'
export type UploadEditsProviderProps = {
children: React.ReactNode
initialUploadEdits?: UploadEdits
}
export type UploadEditsContext = {
getUploadEdits: () => UploadEdits
resetUploadEdits: () => void
updateUploadEdits: (edits: UploadEdits) => void
uploadEdits: UploadEdits
}
const Context = React.createContext<UploadEditsContext>({
getUploadEdits: () => undefined,
resetUploadEdits: undefined,
updateUploadEdits: undefined,
uploadEdits: undefined,
})
export const UploadEditsProvider = ({ children }) => {
const [uploadEdits, setUploadEdits] = React.useState<UploadEdits>(undefined)
export const UploadEditsProvider = ({ children, initialUploadEdits }: UploadEditsProviderProps) => {
const [uploadEdits, setUploadEdits] = React.useState<UploadEdits>(initialUploadEdits || {})
const resetUploadEdits = () => {
setUploadEdits({})
@@ -29,7 +35,13 @@ export const UploadEditsProvider = ({ children }) => {
}))
}
return <Context value={{ resetUploadEdits, updateUploadEdits, uploadEdits }}>{children}</Context>
const getUploadEdits = () => uploadEdits
return (
<Context value={{ getUploadEdits, resetUploadEdits, updateUploadEdits, uploadEdits }}>
{children}
</Context>
)
}
export const useUploadEdits = (): UploadEditsContext => React.use(Context)

View File

@@ -325,6 +325,7 @@ export function DefaultEditView({
isLockingEnabled,
setDocumentIsLocked,
startRouteTransition,
redirectAfterCreate,
],
)