fix(ui): disable publish button if form is autosaving (#11343)
Fixes https://github.com/payloadcms/payload/issues/6648 This PR introduces a new `useFormBackgroundProcessing` hook and a corresponding `setBackgroundProcessing` function in the `useForm` hook. Unlike `useFormProcessing` / `setProcessing`, which mark the entire form as read-only, this new approach only disables the Publish button during autosaving, keeping form fields editable for a better user experience. I named it `backgroundProcessing` because it should run behind the scenes without disrupting the user. You could argue that it is a bit more generic than something like `isAutosaving`, but it signals intent: Background = do not disrupt the user.
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
useAllFormFields,
|
||||
useForm,
|
||||
useFormModified,
|
||||
useFormProcessing,
|
||||
useFormSubmitted,
|
||||
} from '../../forms/Form/context.js'
|
||||
import { useDebounce } from '../../hooks/useDebounce.js'
|
||||
@@ -57,7 +58,8 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
||||
const isProcessingRef = useRef(false)
|
||||
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
const { dispatchFields, isValid, setIsValid, setSubmitted } = useForm()
|
||||
const { dispatchFields, isValid, setBackgroundProcessing, setIsValid, setSubmitted } = useForm()
|
||||
const isFormProcessing = useFormProcessing()
|
||||
|
||||
const [fields] = useAllFormFields()
|
||||
const modified = useFormModified()
|
||||
@@ -108,6 +110,13 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
||||
return
|
||||
}
|
||||
|
||||
// Do not autosave if the form is already processing (e.g. if the user clicked the publish button
|
||||
// right before this autosave runs), as parallel updates could cause conflicts
|
||||
if (isFormProcessing) {
|
||||
queueRef.current = []
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidRef.current) {
|
||||
// Clear queue so we don't end up in an infinite loop
|
||||
queueRef.current = []
|
||||
@@ -120,15 +129,18 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
||||
const latestAction = queueRef.current[queueRef.current.length - 1]
|
||||
queueRef.current = []
|
||||
|
||||
setBackgroundProcessing(true)
|
||||
try {
|
||||
await latestAction()
|
||||
} finally {
|
||||
isProcessingRef.current = false
|
||||
setBackgroundProcessing(false)
|
||||
if (queueRef.current.length > 0) {
|
||||
await processQueue()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
setBackgroundProcessing(false)
|
||||
}, [isFormProcessing, setBackgroundProcessing])
|
||||
|
||||
const autosaveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ export {
|
||||
useAllFormFields,
|
||||
useDocumentForm,
|
||||
useForm,
|
||||
useFormBackgroundProcessing,
|
||||
useFormFields,
|
||||
useFormInitializing,
|
||||
useFormModified,
|
||||
|
||||
@@ -15,6 +15,11 @@ const DocumentFormContext = createContext({} as Context)
|
||||
const FormWatchContext = createContext({} as Context)
|
||||
const SubmittedContext = createContext(false)
|
||||
const ProcessingContext = createContext(false)
|
||||
/**
|
||||
* If the form has started processing in the background (e.g.
|
||||
* if autosave is running), this will be true.
|
||||
*/
|
||||
const BackgroundProcessingContext = createContext(false)
|
||||
const ModifiedContext = createContext(false)
|
||||
const InitializingContext = createContext(false)
|
||||
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
||||
@@ -36,6 +41,11 @@ const useDocumentForm = (): Context => useContext(DocumentFormContext)
|
||||
const useWatchForm = (): Context => useContext(FormWatchContext)
|
||||
const useFormSubmitted = (): boolean => useContext(SubmittedContext)
|
||||
const useFormProcessing = (): boolean => useContext(ProcessingContext)
|
||||
/**
|
||||
* If the form has started processing in the background (e.g.
|
||||
* if autosave is running), this will be true.
|
||||
*/
|
||||
const useFormBackgroundProcessing = (): boolean => useContext(BackgroundProcessingContext)
|
||||
const useFormModified = (): boolean => useContext(ModifiedContext)
|
||||
const useFormInitializing = (): boolean => useContext(InitializingContext)
|
||||
|
||||
@@ -56,6 +66,7 @@ const useFormFields = <Value = unknown>(
|
||||
const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext)
|
||||
|
||||
export {
|
||||
BackgroundProcessingContext,
|
||||
DocumentFormContext,
|
||||
FormContext,
|
||||
FormFieldsContext,
|
||||
@@ -67,6 +78,7 @@ export {
|
||||
useAllFormFields,
|
||||
useDocumentForm,
|
||||
useForm,
|
||||
useFormBackgroundProcessing,
|
||||
useFormFields,
|
||||
useFormInitializing,
|
||||
useFormModified,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { useUploadHandlers } from '../../providers/UploadHandlers/index.js'
|
||||
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import {
|
||||
BackgroundProcessingContext,
|
||||
DocumentFormContext,
|
||||
FormContext,
|
||||
FormFieldsContext,
|
||||
@@ -105,6 +106,8 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
const [isValid, setIsValid] = useState(true)
|
||||
const [initializing, setInitializing] = useState(initializingFromProps)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const [backgroundProcessing, setBackgroundProcessing] = useState(false)
|
||||
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
const contextRef = useRef({} as FormContextType)
|
||||
@@ -653,6 +656,8 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
contextRef.current.createFormData = createFormData
|
||||
contextRef.current.setModified = setModified
|
||||
contextRef.current.setProcessing = setProcessing
|
||||
contextRef.current.setBackgroundProcessing = setBackgroundProcessing
|
||||
|
||||
contextRef.current.setSubmitted = setSubmitted
|
||||
contextRef.current.setIsValid = setIsValid
|
||||
contextRef.current.disabled = disabled
|
||||
@@ -798,11 +803,13 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
<SubmittedContext.Provider value={submitted}>
|
||||
<InitializingContext.Provider value={!isMounted || (isMounted && initializing)}>
|
||||
<ProcessingContext.Provider value={processing}>
|
||||
<BackgroundProcessingContext.Provider value={backgroundProcessing}>
|
||||
<ModifiedContext.Provider value={modified}>
|
||||
<FormFieldsContext.Provider value={fieldsReducer}>
|
||||
{children}
|
||||
</FormFieldsContext.Provider>
|
||||
</ModifiedContext.Provider>
|
||||
</BackgroundProcessingContext.Provider>
|
||||
</ProcessingContext.Provider>
|
||||
</InitializingContext.Provider>
|
||||
</SubmittedContext.Provider>
|
||||
|
||||
@@ -22,6 +22,7 @@ const createFormData: CreateFormData = () => undefined
|
||||
|
||||
const setModified: SetModified = () => undefined
|
||||
const setProcessing: SetProcessing = () => undefined
|
||||
const setBackgroundProcessing: SetProcessing = () => undefined
|
||||
const setSubmitted: SetSubmitted = () => undefined
|
||||
const reset: Reset = () => undefined
|
||||
|
||||
@@ -44,6 +45,7 @@ export const initContextState: Context = {
|
||||
replaceFieldRow: () => undefined,
|
||||
replaceState: () => undefined,
|
||||
reset,
|
||||
setBackgroundProcessing,
|
||||
setDisabled: () => undefined,
|
||||
setIsValid: () => undefined,
|
||||
setModified,
|
||||
|
||||
@@ -248,6 +248,11 @@ export type Context = {
|
||||
}) => void
|
||||
replaceState: (state: FormState) => void
|
||||
reset: Reset
|
||||
/**
|
||||
* If the form has started processing in the background (e.g.
|
||||
* if autosave is running), this will be true.
|
||||
*/
|
||||
setBackgroundProcessing: SetProcessing
|
||||
setDisabled: (disabled: boolean) => void
|
||||
setIsValid: (processing: boolean) => void
|
||||
setModified: SetModified
|
||||
|
||||
@@ -4,7 +4,12 @@ import React from 'react'
|
||||
import type { Props } from '../../elements/Button/types.js'
|
||||
|
||||
import { Button } from '../../elements/Button/index.js'
|
||||
import { useForm, useFormInitializing, useFormProcessing } from '../Form/context.js'
|
||||
import {
|
||||
useForm,
|
||||
useFormBackgroundProcessing,
|
||||
useFormInitializing,
|
||||
useFormProcessing,
|
||||
} from '../Form/context.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'form-submit'
|
||||
@@ -21,10 +26,17 @@ export const FormSubmit: React.FC<Props> = (props) => {
|
||||
} = props
|
||||
|
||||
const processing = useFormProcessing()
|
||||
const backgroundProcessing = useFormBackgroundProcessing()
|
||||
const initializing = useFormInitializing()
|
||||
const { disabled, submit } = useForm()
|
||||
|
||||
const canSave = !(disabledFromProps || initializing || processing || disabled)
|
||||
const canSave = !(
|
||||
disabledFromProps ||
|
||||
initializing ||
|
||||
processing ||
|
||||
backgroundProcessing ||
|
||||
disabled
|
||||
)
|
||||
|
||||
const handleClick =
|
||||
onClick ??
|
||||
|
||||
Reference in New Issue
Block a user