From 7118b6418f0188aef889bc614045e4b2b5014927 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 27 Feb 2025 10:28:08 -0700 Subject: [PATCH] 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. --- packages/ui/src/elements/Autosave/index.tsx | 16 ++++++++++++++-- packages/ui/src/exports/client/index.ts | 1 + packages/ui/src/forms/Form/context.ts | 12 ++++++++++++ packages/ui/src/forms/Form/index.tsx | 17 ++++++++++++----- packages/ui/src/forms/Form/initContextState.ts | 2 ++ packages/ui/src/forms/Form/types.ts | 5 +++++ packages/ui/src/forms/Submit/index.tsx | 16 ++++++++++++++-- 7 files changed, 60 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/elements/Autosave/index.tsx b/packages/ui/src/elements/Autosave/index.tsx index 0d9d30d3a..4ddac35fb 100644 --- a/packages/ui/src/elements/Autosave/index.tsx +++ b/packages/ui/src/elements/Autosave/index.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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(null) diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 870044f2e..1d1a53d72 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -192,6 +192,7 @@ export { useAllFormFields, useDocumentForm, useForm, + useFormBackgroundProcessing, useFormFields, useFormInitializing, useFormModified, diff --git a/packages/ui/src/forms/Form/context.ts b/packages/ui/src/forms/Form/context.ts index 6c520d93a..28bba76b2 100644 --- a/packages/ui/src/forms/Form/context.ts +++ b/packages/ui/src/forms/Form/context.ts @@ -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([{}, () => 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 = ( const useAllFormFields = (): FormFieldsContextType => useFullContext(FormFieldsContext) export { + BackgroundProcessingContext, DocumentFormContext, FormContext, FormFieldsContext, @@ -67,6 +78,7 @@ export { useAllFormFields, useDocumentForm, useForm, + useFormBackgroundProcessing, useFormFields, useFormInitializing, useFormModified, diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 5fc0f2cd2..8e8bc2c2b 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -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 = (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(null) const contextRef = useRef({} as FormContextType) @@ -653,6 +656,8 @@ export const Form: React.FC = (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 = (props) => { - - - {children} - - + + + + {children} + + + diff --git a/packages/ui/src/forms/Form/initContextState.ts b/packages/ui/src/forms/Form/initContextState.ts index 3f49c2a31..30ba7295c 100644 --- a/packages/ui/src/forms/Form/initContextState.ts +++ b/packages/ui/src/forms/Form/initContextState.ts @@ -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, diff --git a/packages/ui/src/forms/Form/types.ts b/packages/ui/src/forms/Form/types.ts index 5455e189a..f68a23eeb 100644 --- a/packages/ui/src/forms/Form/types.ts +++ b/packages/ui/src/forms/Form/types.ts @@ -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 diff --git a/packages/ui/src/forms/Submit/index.tsx b/packages/ui/src/forms/Submit/index.tsx index ecf5ed4f2..6557ccd4f 100644 --- a/packages/ui/src/forms/Submit/index.tsx +++ b/packages/ui/src/forms/Submit/index.tsx @@ -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 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 ??