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,
|
useAllFormFields,
|
||||||
useForm,
|
useForm,
|
||||||
useFormModified,
|
useFormModified,
|
||||||
|
useFormProcessing,
|
||||||
useFormSubmitted,
|
useFormSubmitted,
|
||||||
} from '../../forms/Form/context.js'
|
} from '../../forms/Form/context.js'
|
||||||
import { useDebounce } from '../../hooks/useDebounce.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 isProcessingRef = useRef(false)
|
||||||
|
|
||||||
const { reportUpdate } = useDocumentEvents()
|
const { reportUpdate } = useDocumentEvents()
|
||||||
const { dispatchFields, isValid, setIsValid, setSubmitted } = useForm()
|
const { dispatchFields, isValid, setBackgroundProcessing, setIsValid, setSubmitted } = useForm()
|
||||||
|
const isFormProcessing = useFormProcessing()
|
||||||
|
|
||||||
const [fields] = useAllFormFields()
|
const [fields] = useAllFormFields()
|
||||||
const modified = useFormModified()
|
const modified = useFormModified()
|
||||||
@@ -108,6 +110,13 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
|||||||
return
|
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) {
|
if (!isValidRef.current) {
|
||||||
// Clear queue so we don't end up in an infinite loop
|
// Clear queue so we don't end up in an infinite loop
|
||||||
queueRef.current = []
|
queueRef.current = []
|
||||||
@@ -120,15 +129,18 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
|||||||
const latestAction = queueRef.current[queueRef.current.length - 1]
|
const latestAction = queueRef.current[queueRef.current.length - 1]
|
||||||
queueRef.current = []
|
queueRef.current = []
|
||||||
|
|
||||||
|
setBackgroundProcessing(true)
|
||||||
try {
|
try {
|
||||||
await latestAction()
|
await latestAction()
|
||||||
} finally {
|
} finally {
|
||||||
isProcessingRef.current = false
|
isProcessingRef.current = false
|
||||||
|
setBackgroundProcessing(false)
|
||||||
if (queueRef.current.length > 0) {
|
if (queueRef.current.length > 0) {
|
||||||
await processQueue()
|
await processQueue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
setBackgroundProcessing(false)
|
||||||
|
}, [isFormProcessing, setBackgroundProcessing])
|
||||||
|
|
||||||
const autosaveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const autosaveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export {
|
|||||||
useAllFormFields,
|
useAllFormFields,
|
||||||
useDocumentForm,
|
useDocumentForm,
|
||||||
useForm,
|
useForm,
|
||||||
|
useFormBackgroundProcessing,
|
||||||
useFormFields,
|
useFormFields,
|
||||||
useFormInitializing,
|
useFormInitializing,
|
||||||
useFormModified,
|
useFormModified,
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ const DocumentFormContext = createContext({} as Context)
|
|||||||
const FormWatchContext = createContext({} as Context)
|
const FormWatchContext = createContext({} as Context)
|
||||||
const SubmittedContext = createContext(false)
|
const SubmittedContext = createContext(false)
|
||||||
const ProcessingContext = 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 ModifiedContext = createContext(false)
|
||||||
const InitializingContext = createContext(false)
|
const InitializingContext = createContext(false)
|
||||||
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
||||||
@@ -36,6 +41,11 @@ const useDocumentForm = (): Context => useContext(DocumentFormContext)
|
|||||||
const useWatchForm = (): Context => useContext(FormWatchContext)
|
const useWatchForm = (): Context => useContext(FormWatchContext)
|
||||||
const useFormSubmitted = (): boolean => useContext(SubmittedContext)
|
const useFormSubmitted = (): boolean => useContext(SubmittedContext)
|
||||||
const useFormProcessing = (): boolean => useContext(ProcessingContext)
|
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 useFormModified = (): boolean => useContext(ModifiedContext)
|
||||||
const useFormInitializing = (): boolean => useContext(InitializingContext)
|
const useFormInitializing = (): boolean => useContext(InitializingContext)
|
||||||
|
|
||||||
@@ -56,6 +66,7 @@ const useFormFields = <Value = unknown>(
|
|||||||
const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext)
|
const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext)
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
BackgroundProcessingContext,
|
||||||
DocumentFormContext,
|
DocumentFormContext,
|
||||||
FormContext,
|
FormContext,
|
||||||
FormFieldsContext,
|
FormFieldsContext,
|
||||||
@@ -67,6 +78,7 @@ export {
|
|||||||
useAllFormFields,
|
useAllFormFields,
|
||||||
useDocumentForm,
|
useDocumentForm,
|
||||||
useForm,
|
useForm,
|
||||||
|
useFormBackgroundProcessing,
|
||||||
useFormFields,
|
useFormFields,
|
||||||
useFormInitializing,
|
useFormInitializing,
|
||||||
useFormModified,
|
useFormModified,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { useUploadHandlers } from '../../providers/UploadHandlers/index.js'
|
|||||||
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js'
|
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js'
|
||||||
import { requests } from '../../utilities/api.js'
|
import { requests } from '../../utilities/api.js'
|
||||||
import {
|
import {
|
||||||
|
BackgroundProcessingContext,
|
||||||
DocumentFormContext,
|
DocumentFormContext,
|
||||||
FormContext,
|
FormContext,
|
||||||
FormFieldsContext,
|
FormFieldsContext,
|
||||||
@@ -105,6 +106,8 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
const [isValid, setIsValid] = useState(true)
|
const [isValid, setIsValid] = useState(true)
|
||||||
const [initializing, setInitializing] = useState(initializingFromProps)
|
const [initializing, setInitializing] = useState(initializingFromProps)
|
||||||
const [processing, setProcessing] = useState(false)
|
const [processing, setProcessing] = useState(false)
|
||||||
|
const [backgroundProcessing, setBackgroundProcessing] = useState(false)
|
||||||
|
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const contextRef = useRef({} as FormContextType)
|
const contextRef = useRef({} as FormContextType)
|
||||||
@@ -653,6 +656,8 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
contextRef.current.createFormData = createFormData
|
contextRef.current.createFormData = createFormData
|
||||||
contextRef.current.setModified = setModified
|
contextRef.current.setModified = setModified
|
||||||
contextRef.current.setProcessing = setProcessing
|
contextRef.current.setProcessing = setProcessing
|
||||||
|
contextRef.current.setBackgroundProcessing = setBackgroundProcessing
|
||||||
|
|
||||||
contextRef.current.setSubmitted = setSubmitted
|
contextRef.current.setSubmitted = setSubmitted
|
||||||
contextRef.current.setIsValid = setIsValid
|
contextRef.current.setIsValid = setIsValid
|
||||||
contextRef.current.disabled = disabled
|
contextRef.current.disabled = disabled
|
||||||
@@ -798,11 +803,13 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
<SubmittedContext.Provider value={submitted}>
|
<SubmittedContext.Provider value={submitted}>
|
||||||
<InitializingContext.Provider value={!isMounted || (isMounted && initializing)}>
|
<InitializingContext.Provider value={!isMounted || (isMounted && initializing)}>
|
||||||
<ProcessingContext.Provider value={processing}>
|
<ProcessingContext.Provider value={processing}>
|
||||||
<ModifiedContext.Provider value={modified}>
|
<BackgroundProcessingContext.Provider value={backgroundProcessing}>
|
||||||
<FormFieldsContext.Provider value={fieldsReducer}>
|
<ModifiedContext.Provider value={modified}>
|
||||||
{children}
|
<FormFieldsContext.Provider value={fieldsReducer}>
|
||||||
</FormFieldsContext.Provider>
|
{children}
|
||||||
</ModifiedContext.Provider>
|
</FormFieldsContext.Provider>
|
||||||
|
</ModifiedContext.Provider>
|
||||||
|
</BackgroundProcessingContext.Provider>
|
||||||
</ProcessingContext.Provider>
|
</ProcessingContext.Provider>
|
||||||
</InitializingContext.Provider>
|
</InitializingContext.Provider>
|
||||||
</SubmittedContext.Provider>
|
</SubmittedContext.Provider>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const createFormData: CreateFormData = () => undefined
|
|||||||
|
|
||||||
const setModified: SetModified = () => undefined
|
const setModified: SetModified = () => undefined
|
||||||
const setProcessing: SetProcessing = () => undefined
|
const setProcessing: SetProcessing = () => undefined
|
||||||
|
const setBackgroundProcessing: SetProcessing = () => undefined
|
||||||
const setSubmitted: SetSubmitted = () => undefined
|
const setSubmitted: SetSubmitted = () => undefined
|
||||||
const reset: Reset = () => undefined
|
const reset: Reset = () => undefined
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ export const initContextState: Context = {
|
|||||||
replaceFieldRow: () => undefined,
|
replaceFieldRow: () => undefined,
|
||||||
replaceState: () => undefined,
|
replaceState: () => undefined,
|
||||||
reset,
|
reset,
|
||||||
|
setBackgroundProcessing,
|
||||||
setDisabled: () => undefined,
|
setDisabled: () => undefined,
|
||||||
setIsValid: () => undefined,
|
setIsValid: () => undefined,
|
||||||
setModified,
|
setModified,
|
||||||
|
|||||||
@@ -248,6 +248,11 @@ export type Context = {
|
|||||||
}) => void
|
}) => void
|
||||||
replaceState: (state: FormState) => void
|
replaceState: (state: FormState) => void
|
||||||
reset: Reset
|
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
|
setDisabled: (disabled: boolean) => void
|
||||||
setIsValid: (processing: boolean) => void
|
setIsValid: (processing: boolean) => void
|
||||||
setModified: SetModified
|
setModified: SetModified
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import React from 'react'
|
|||||||
import type { Props } from '../../elements/Button/types.js'
|
import type { Props } from '../../elements/Button/types.js'
|
||||||
|
|
||||||
import { Button } from '../../elements/Button/index.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'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'form-submit'
|
const baseClass = 'form-submit'
|
||||||
@@ -21,10 +26,17 @@ export const FormSubmit: React.FC<Props> = (props) => {
|
|||||||
} = props
|
} = props
|
||||||
|
|
||||||
const processing = useFormProcessing()
|
const processing = useFormProcessing()
|
||||||
|
const backgroundProcessing = useFormBackgroundProcessing()
|
||||||
const initializing = useFormInitializing()
|
const initializing = useFormInitializing()
|
||||||
const { disabled, submit } = useForm()
|
const { disabled, submit } = useForm()
|
||||||
|
|
||||||
const canSave = !(disabledFromProps || initializing || processing || disabled)
|
const canSave = !(
|
||||||
|
disabledFromProps ||
|
||||||
|
initializing ||
|
||||||
|
processing ||
|
||||||
|
backgroundProcessing ||
|
||||||
|
disabled
|
||||||
|
)
|
||||||
|
|
||||||
const handleClick =
|
const handleClick =
|
||||||
onClick ??
|
onClick ??
|
||||||
|
|||||||
Reference in New Issue
Block a user