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:
@@ -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>
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { UploadProps_v4 } from '../../Upload/index.js'
|
||||
|
||||
export type EditFormProps = {
|
||||
readonly submitted?: boolean
|
||||
}
|
||||
} & Pick<UploadProps_v4, 'resetUploadEdits' | 'updateUploadEdits' | 'uploadEdits'>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -325,6 +325,7 @@ export function DefaultEditView({
|
||||
isLockingEnabled,
|
||||
setDocumentIsLocked,
|
||||
startRouteTransition,
|
||||
redirectAfterCreate,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user