fix(ui): autosave hooks are not reflected in form state (#13416)
Fixes #10515. Needed for #12956. Hooks run within autosave are not reflected in form state. Similar to #10268, but for autosave events. For example, if you are using a computed value, like this: ```ts [ // ... { name: 'title', type: 'text', }, { name: 'computedTitle', type: 'text', hooks: { beforeChange: [({ data }) => data?.title], }, }, ] ``` In the example above, when an autosave event is triggered after changing the `title` field, we expect the `computedTitle` field to match. But although this takes place on the database level, the UI does not reflect this change unless you refresh the page or navigate back and forth. Here's an example: Before: https://github.com/user-attachments/assets/c8c68a78-9957-45a8-a710-84d954d15bcc After: https://github.com/user-attachments/assets/16cb87a5-83ca-4891-b01f-f5c4b0a34362 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210561273449855
This commit is contained in:
@@ -5,7 +5,6 @@ import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
|
||||
import { dequal } from 'dequal/lite'
|
||||
import { reduceFieldsToValues, versionDefaults } from 'payload/shared'
|
||||
import React, { useDeferredValue, useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
useAllFormFields,
|
||||
@@ -17,13 +16,11 @@ import { useDebounce } from '../../hooks/useDebounce.js'
|
||||
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
||||
import { useQueues } from '../../hooks/useQueues.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useDocumentEvents } from '../../providers/DocumentEvents/index.js'
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js'
|
||||
import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFieldsToValuesWithValidation.js'
|
||||
import { useDocumentDrawerContext } from '../DocumentDrawer/Provider.js'
|
||||
import { LeaveWithoutSaving } from '../LeaveWithoutSaving/index.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -51,16 +48,11 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
||||
incrementVersionCount,
|
||||
lastUpdateTime,
|
||||
mostRecentVersionIsAutosaved,
|
||||
setLastUpdateTime,
|
||||
setMostRecentVersionIsAutosaved,
|
||||
setUnpublishedVersionCount,
|
||||
updateSavedDocumentData,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const { onSave: onSaveFromDocumentDrawer } = useDocumentDrawerContext()
|
||||
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
const { dispatchFields, isValid, setBackgroundProcessing, setIsValid } = useForm()
|
||||
const { isValid, setBackgroundProcessing, submit } = useForm()
|
||||
|
||||
const [formState] = useAllFormFields()
|
||||
const modified = useFormModified()
|
||||
@@ -151,118 +143,38 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
||||
method = 'POST'
|
||||
}
|
||||
|
||||
if (url) {
|
||||
if (modifiedRef.current) {
|
||||
const { data, valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true)
|
||||
const { valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true)
|
||||
|
||||
data._status = 'draft'
|
||||
const skipSubmission =
|
||||
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
|
||||
|
||||
const skipSubmission =
|
||||
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
|
||||
if (!skipSubmission && modifiedRef.current && url) {
|
||||
const result = await submit({
|
||||
action: url,
|
||||
context: {
|
||||
incrementVersionCount: false,
|
||||
},
|
||||
disableFormWhileProcessing: false,
|
||||
disableSuccessStatus: true,
|
||||
method,
|
||||
overrides: {
|
||||
_status: 'draft',
|
||||
},
|
||||
skipValidation: versionsConfig?.drafts && !versionsConfig?.drafts?.validate,
|
||||
})
|
||||
|
||||
if (!skipSubmission) {
|
||||
let res
|
||||
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
})
|
||||
} catch (_err) {
|
||||
// Swallow Error
|
||||
}
|
||||
|
||||
const newDate = new Date()
|
||||
// We need to log the time in order to figure out if we need to trigger the state off later
|
||||
endTimestamp = newDate.getTime()
|
||||
|
||||
const json = await res.json()
|
||||
|
||||
if (res.status === 200) {
|
||||
setLastUpdateTime(newDate.getTime())
|
||||
|
||||
reportUpdate({
|
||||
id,
|
||||
entitySlug,
|
||||
updatedAt: newDate.toISOString(),
|
||||
})
|
||||
|
||||
// if onSaveFromDocumentDrawer is defined, call it
|
||||
if (typeof onSaveFromDocumentDrawer === 'function') {
|
||||
void onSaveFromDocumentDrawer({
|
||||
...json,
|
||||
operation: 'update',
|
||||
})
|
||||
}
|
||||
|
||||
if (!mostRecentVersionIsAutosaved) {
|
||||
incrementVersionCount()
|
||||
setMostRecentVersionIsAutosaved(true)
|
||||
setUnpublishedVersionCount((prev) => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (versionsConfig?.drafts && versionsConfig?.drafts?.validate && json?.errors) {
|
||||
if (Array.isArray(json.errors)) {
|
||||
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
|
||||
([fieldErrs, nonFieldErrs], err) => {
|
||||
const newFieldErrs = []
|
||||
const newNonFieldErrs = []
|
||||
|
||||
if (err?.message) {
|
||||
newNonFieldErrs.push(err)
|
||||
}
|
||||
|
||||
if (Array.isArray(err?.data)) {
|
||||
err.data.forEach((dataError) => {
|
||||
if (dataError?.field) {
|
||||
newFieldErrs.push(dataError)
|
||||
} else {
|
||||
newNonFieldErrs.push(dataError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
[...fieldErrs, ...newFieldErrs],
|
||||
[...nonFieldErrs, ...newNonFieldErrs],
|
||||
]
|
||||
},
|
||||
[[], []],
|
||||
)
|
||||
|
||||
dispatchFields({
|
||||
type: 'ADD_SERVER_ERRORS',
|
||||
errors: fieldErrors,
|
||||
})
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
toast.error(err.message || i18n.t('error:unknown'))
|
||||
})
|
||||
|
||||
setIsValid(false)
|
||||
hideIndicator()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// If it's not an error then we can update the document data inside the context
|
||||
const document = json?.doc || json?.result
|
||||
|
||||
// Manually update the data since this function doesn't fire the `submit` function from useForm
|
||||
if (document) {
|
||||
setIsValid(true)
|
||||
updateSavedDocumentData(document)
|
||||
}
|
||||
}
|
||||
|
||||
hideIndicator()
|
||||
}
|
||||
if (result && result?.res?.ok && !mostRecentVersionIsAutosaved) {
|
||||
incrementVersionCount()
|
||||
setMostRecentVersionIsAutosaved(true)
|
||||
setUnpublishedVersionCount((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const newDate = new Date()
|
||||
|
||||
// We need to log the time in order to figure out if we need to trigger the state off later
|
||||
endTimestamp = newDate.getTime()
|
||||
|
||||
hideIndicator()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,6 +20,11 @@ export type DocumentDrawerContextProps = {
|
||||
}) => Promise<void> | void
|
||||
readonly onSave?: (args: {
|
||||
collectionConfig?: ClientCollectionConfig
|
||||
/**
|
||||
* @experimental - Note: this property is experimental and may change in the future. Use as your own discretion.
|
||||
* If you want to pass additional data to the onSuccess callback, you can use this context object.
|
||||
*/
|
||||
context?: Record<string, unknown>
|
||||
doc: TypeWithID
|
||||
operation: 'create' | 'update'
|
||||
result: Data
|
||||
|
||||
@@ -161,7 +161,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { setModified } = useForm()
|
||||
const { id, docPermissions, savedDocumentData, setUploadStatus } = useDocumentInfo()
|
||||
const { id, data, docPermissions, setUploadStatus } = useDocumentInfo()
|
||||
const isFormSubmitting = useFormProcessing()
|
||||
const { errorMessage, setValue, showError, value } = useField<File>({
|
||||
path: 'file',
|
||||
@@ -349,7 +349,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
|
||||
const acceptMimeTypes = uploadConfig.mimeTypes?.join(', ')
|
||||
|
||||
const imageCacheTag = uploadConfig?.cacheTags && savedDocumentData?.updatedAt
|
||||
const imageCacheTag = uploadConfig?.cacheTags && data?.updatedAt
|
||||
|
||||
useEffect(() => {
|
||||
const handleControlFileUrl = async () => {
|
||||
@@ -375,11 +375,11 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
return (
|
||||
<div className={[fieldBaseClass, baseClass].filter(Boolean).join(' ')}>
|
||||
<FieldError message={errorMessage} showError={showError} />
|
||||
{savedDocumentData && savedDocumentData.filename && !removedFile && (
|
||||
{data && data.filename && !removedFile && (
|
||||
<FileDetails
|
||||
collectionSlug={collectionSlug}
|
||||
customUploadActions={customActions}
|
||||
doc={savedDocumentData}
|
||||
doc={data}
|
||||
enableAdjustments={showCrop || showFocalPoint}
|
||||
handleRemove={canRemoveUpload ? handleFileRemoval : undefined}
|
||||
hasImageSizes={hasImageSizes}
|
||||
@@ -388,7 +388,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
uploadConfig={uploadConfig}
|
||||
/>
|
||||
)}
|
||||
{((!uploadConfig.hideFileInputOnCreate && !savedDocumentData?.filename) || removedFile) && (
|
||||
{((!uploadConfig.hideFileInputOnCreate && !data?.filename) || removedFile) && (
|
||||
<div className={`${baseClass}__upload`}>
|
||||
{!value && !showUrlInput && (
|
||||
<Dropzone onChange={handleFileSelection}>
|
||||
@@ -506,7 +506,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
<UploadActions
|
||||
customActions={customActions}
|
||||
enableAdjustments={showCrop || showFocalPoint}
|
||||
enablePreviewSizes={hasImageSizes && savedDocumentData?.filename && !removedFile}
|
||||
enablePreviewSizes={hasImageSizes && data?.filename && !removedFile}
|
||||
mimeType={value.type}
|
||||
/>
|
||||
</div>
|
||||
@@ -523,17 +523,17 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(value || savedDocumentData?.filename) && (
|
||||
{(value || data?.filename) && (
|
||||
<EditDepthProvider>
|
||||
<Drawer Header={null} slug={editDrawerSlug}>
|
||||
<EditUpload
|
||||
fileName={value?.name || savedDocumentData?.filename}
|
||||
fileSrc={savedDocumentData?.url || fileSrc}
|
||||
fileName={value?.name || data?.filename}
|
||||
fileSrc={data?.url || fileSrc}
|
||||
imageCacheTag={imageCacheTag}
|
||||
initialCrop={uploadEdits?.crop ?? undefined}
|
||||
initialFocalPoint={{
|
||||
x: uploadEdits?.focalPoint?.x || savedDocumentData?.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || savedDocumentData?.focalY || 50,
|
||||
x: uploadEdits?.focalPoint?.x || data?.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || data?.focalY || 50,
|
||||
}}
|
||||
onSave={onEditsSave}
|
||||
showCrop={showCrop}
|
||||
@@ -542,18 +542,14 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
|
||||
</Drawer>
|
||||
</EditDepthProvider>
|
||||
)}
|
||||
{savedDocumentData && hasImageSizes && (
|
||||
{data && hasImageSizes && (
|
||||
<Drawer
|
||||
className={`${baseClass}__previewDrawer`}
|
||||
hoverTitle
|
||||
slug={sizePreviewSlug}
|
||||
title={t('upload:sizesFor', { label: savedDocumentData.filename })}
|
||||
title={t('upload:sizesFor', { label: data.filename })}
|
||||
>
|
||||
<PreviewSizes
|
||||
doc={savedDocumentData}
|
||||
imageCacheTag={imageCacheTag}
|
||||
uploadConfig={uploadConfig}
|
||||
/>
|
||||
<PreviewSizes doc={data} imageCacheTag={imageCacheTag} uploadConfig={uploadConfig} />
|
||||
</Drawer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
Context as FormContextType,
|
||||
FormProps,
|
||||
GetDataByPath,
|
||||
Submit,
|
||||
SubmitOptions,
|
||||
} from './types.js'
|
||||
|
||||
@@ -199,14 +200,19 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
return isValid
|
||||
}, [collectionSlug, config, dispatchFields, id, operation, t, user, documentForm])
|
||||
|
||||
const submit = useCallback(
|
||||
async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||
const submit = useCallback<Submit>(
|
||||
async (options, e) => {
|
||||
const {
|
||||
action: actionArg = action,
|
||||
context,
|
||||
disableFormWhileProcessing = true,
|
||||
disableSuccessStatus: disableSuccessStatusFromArgs,
|
||||
method: methodToUse = method,
|
||||
overrides: overridesFromArgs = {},
|
||||
skipValidation,
|
||||
} = options
|
||||
} = options || ({} as SubmitOptions)
|
||||
|
||||
const disableToast = disableSuccessStatusFromArgs ?? disableSuccessStatus
|
||||
|
||||
if (disabled) {
|
||||
if (e) {
|
||||
@@ -217,6 +223,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
|
||||
// create new toast promise which will resolve manually later
|
||||
let errorToast, successToast
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
successToast = resolve
|
||||
errorToast = reject
|
||||
@@ -225,7 +232,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
const hasFormSubmitAction =
|
||||
actionArg || typeof action === 'string' || typeof action === 'function'
|
||||
|
||||
if (redirect || disableSuccessStatus || !hasFormSubmitAction) {
|
||||
if (redirect || disableToast || !hasFormSubmitAction) {
|
||||
// Do not show submitting toast, as the promise toast may never disappear under these conditions.
|
||||
// Instead, make successToast() or errorToast() throw toast.success / toast.error
|
||||
successToast = (data) => toast.success(data)
|
||||
@@ -247,8 +254,10 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
setProcessing(true)
|
||||
setDisabled(true)
|
||||
if (disableFormWhileProcessing) {
|
||||
setProcessing(true)
|
||||
setDisabled(true)
|
||||
}
|
||||
|
||||
if (waitForAutocomplete) {
|
||||
await wait(100)
|
||||
@@ -290,6 +299,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
skipValidation || disableValidationOnSubmit ? true : await contextRef.current.validateForm()
|
||||
|
||||
setIsValid(isValid)
|
||||
|
||||
// If not valid, prevent submission
|
||||
if (!isValid) {
|
||||
errorToast(t('error:correctInvalidFields'))
|
||||
@@ -366,9 +376,10 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
if (isJSON) {
|
||||
json = await res.json()
|
||||
}
|
||||
|
||||
if (res.status < 400) {
|
||||
if (typeof onSuccess === 'function') {
|
||||
const newFormState = await onSuccess(json)
|
||||
const newFormState = await onSuccess(json, context)
|
||||
|
||||
if (newFormState) {
|
||||
dispatchFields({
|
||||
@@ -379,12 +390,13 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setSubmitted(false)
|
||||
setProcessing(false)
|
||||
|
||||
if (redirect) {
|
||||
startRouteTransition(() => router.push(redirect))
|
||||
} else if (!disableSuccessStatus) {
|
||||
} else if (!disableToast) {
|
||||
successToast(json.message || t('general:submissionSuccessful'))
|
||||
}
|
||||
} else {
|
||||
@@ -392,6 +404,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
setSubmitted(true)
|
||||
|
||||
contextRef.current = { ...contextRef.current } // triggers rerender of all components that subscribe to form
|
||||
|
||||
if (json.message) {
|
||||
errorToast(json.message)
|
||||
return
|
||||
@@ -443,6 +456,8 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
|
||||
errorToast(message)
|
||||
}
|
||||
|
||||
return { formState: contextRef.current.fields, res }
|
||||
} catch (err) {
|
||||
console.error('Error submitting form', err) // eslint-disable-line no-console
|
||||
setProcessing(false)
|
||||
|
||||
@@ -52,7 +52,7 @@ export type FormProps = {
|
||||
log?: boolean
|
||||
onChange?: ((args: { formState: FormState; submitted?: boolean }) => Promise<FormState>)[]
|
||||
onSubmit?: (fields: FormState, data: Data) => void
|
||||
onSuccess?: (json: unknown) => Promise<FormState | void> | void
|
||||
onSuccess?: (json: unknown, context?: Record<string, unknown>) => Promise<FormState | void> | void
|
||||
redirect?: string
|
||||
submitted?: boolean
|
||||
uuid?: string
|
||||
@@ -70,16 +70,40 @@ export type FormProps = {
|
||||
|
||||
export type SubmitOptions = {
|
||||
action?: string
|
||||
/**
|
||||
* @experimental - Note: this property is experimental and may change in the future. Use as your own discretion.
|
||||
* If you want to pass additional data to the onSuccess callback, you can use this context object.
|
||||
*/
|
||||
context?: Record<string, unknown>
|
||||
/**
|
||||
* When true, will disable the form while it is processing.
|
||||
* @default true
|
||||
*/
|
||||
disableFormWhileProcessing?: boolean
|
||||
/**
|
||||
* When true, will disable the success toast after form submission.
|
||||
* @default false
|
||||
*/
|
||||
disableSuccessStatus?: boolean
|
||||
method?: string
|
||||
overrides?: ((formState) => FormData) | Record<string, unknown>
|
||||
/**
|
||||
* When true, will skip validation before submitting the form.
|
||||
* @default false
|
||||
*/
|
||||
skipValidation?: boolean
|
||||
}
|
||||
|
||||
export type DispatchFields = React.Dispatch<any>
|
||||
|
||||
export type Submit = (
|
||||
options?: SubmitOptions,
|
||||
e?: React.FormEvent<HTMLFormElement>,
|
||||
) => Promise<void>
|
||||
) => Promise</**
|
||||
* @experimental - Note: the `{ res: ... }` return type is experimental and may change in the future. Use as your own discretion.
|
||||
* Returns the form state and the response from the server.
|
||||
*/
|
||||
{ formState?: FormState; res: Response } | void>
|
||||
|
||||
export type ValidateForm = () => Promise<boolean>
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ const DocumentInfo: React.FC<
|
||||
const [versionCount, setVersionCount] = useState(versionCountFromProps)
|
||||
|
||||
const [hasPublishedDoc, setHasPublishedDoc] = useState(hasPublishedDocFromProps)
|
||||
|
||||
const [unpublishedVersionCount, setUnpublishedVersionCount] = useState(
|
||||
unpublishedVersionCountFromProps,
|
||||
)
|
||||
@@ -104,11 +105,14 @@ const DocumentInfo: React.FC<
|
||||
const [documentIsLocked, setDocumentIsLocked] = useControllableState<boolean | undefined>(
|
||||
isLockedFromProps,
|
||||
)
|
||||
|
||||
const [currentEditor, setCurrentEditor] = useControllableState<ClientUser | null>(
|
||||
currentEditorFromProps,
|
||||
)
|
||||
const [lastUpdateTime, setLastUpdateTime] = useControllableState<number>(lastUpdateTimeFromProps)
|
||||
const [savedDocumentData, setSavedDocumentData] = useControllableState(initialData)
|
||||
|
||||
const [data, setData] = useControllableState(initialData)
|
||||
|
||||
const [uploadStatus, setUploadStatus] = useControllableState<'failed' | 'idle' | 'uploading'>(
|
||||
'idle',
|
||||
)
|
||||
@@ -294,13 +298,6 @@ const DocumentInfo: React.FC<
|
||||
}
|
||||
}, [collectionConfig, globalConfig, versionCount])
|
||||
|
||||
const updateSavedDocumentData = React.useCallback<DocumentInfoContext['updateSavedDocumentData']>(
|
||||
(json) => {
|
||||
setSavedDocumentData(json)
|
||||
},
|
||||
[setSavedDocumentData],
|
||||
)
|
||||
|
||||
/**
|
||||
* @todo: Remove this in v4
|
||||
* Users should use the `DocumentTitleContext` instead.
|
||||
@@ -309,14 +306,14 @@ const DocumentInfo: React.FC<
|
||||
setDocumentTitle(
|
||||
formatDocTitle({
|
||||
collectionConfig,
|
||||
data: { ...savedDocumentData, id },
|
||||
data: { ...data, id },
|
||||
dateFormat,
|
||||
fallback: id?.toString(),
|
||||
globalConfig,
|
||||
i18n,
|
||||
}),
|
||||
)
|
||||
}, [collectionConfig, globalConfig, savedDocumentData, dateFormat, i18n, id])
|
||||
}, [collectionConfig, globalConfig, data, dateFormat, i18n, id])
|
||||
|
||||
// clean on unmount
|
||||
useEffect(() => {
|
||||
@@ -351,6 +348,7 @@ const DocumentInfo: React.FC<
|
||||
...props,
|
||||
action,
|
||||
currentEditor,
|
||||
data,
|
||||
docConfig,
|
||||
docPermissions,
|
||||
documentIsLocked,
|
||||
@@ -367,8 +365,9 @@ const DocumentInfo: React.FC<
|
||||
lastUpdateTime,
|
||||
mostRecentVersionIsAutosaved,
|
||||
preferencesKey,
|
||||
savedDocumentData,
|
||||
savedDocumentData: data,
|
||||
setCurrentEditor,
|
||||
setData,
|
||||
setDocFieldPreferences,
|
||||
setDocumentIsLocked,
|
||||
setDocumentTitle,
|
||||
@@ -381,7 +380,7 @@ const DocumentInfo: React.FC<
|
||||
unlockDocument,
|
||||
unpublishedVersionCount,
|
||||
updateDocumentEditor,
|
||||
updateSavedDocumentData,
|
||||
updateSavedDocumentData: setData,
|
||||
uploadStatus,
|
||||
versionCount,
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export type DocumentInfoProps = {
|
||||
|
||||
export type DocumentInfoContext = {
|
||||
currentEditor?: ClientUser | null | number | string
|
||||
data?: Data
|
||||
docConfig?: ClientCollectionConfig | ClientGlobalConfig
|
||||
documentIsLocked?: boolean
|
||||
documentLockState: React.RefObject<{
|
||||
@@ -61,21 +62,26 @@ export type DocumentInfoContext = {
|
||||
incrementVersionCount: () => void
|
||||
isInitializing: boolean
|
||||
preferencesKey?: string
|
||||
/**
|
||||
* @deprecated This property is deprecated and will be removed in v4.
|
||||
* Use `data` instead.
|
||||
*/
|
||||
savedDocumentData?: Data
|
||||
setCurrentEditor?: React.Dispatch<React.SetStateAction<ClientUser>>
|
||||
setData: (data: Data) => void
|
||||
setDocFieldPreferences: (
|
||||
field: string,
|
||||
fieldPreferences: { [key: string]: unknown } & Partial<InsideFieldsPreferences>,
|
||||
) => void
|
||||
setDocumentIsLocked?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
/**
|
||||
*
|
||||
* @deprecated This property is deprecated and will be removed in v4.
|
||||
* This is for performance reasons. Use the `DocumentTitleContext` instead.
|
||||
* This is for performance reasons. Use the `DocumentTitleContext` instead
|
||||
* via the `useDocumentTitle` hook.
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useDocumentTitle } from '@payloadcms/ui'
|
||||
* const { setDocumentTitle } = useDocumentTitle()
|
||||
* const { setDocumentTitle } = useDocumentTitle()
|
||||
* ```
|
||||
*/
|
||||
setDocumentTitle: React.Dispatch<React.SetStateAction<string>>
|
||||
@@ -86,17 +92,22 @@ export type DocumentInfoContext = {
|
||||
setUploadStatus?: (status: 'failed' | 'idle' | 'uploading') => void
|
||||
/**
|
||||
* @deprecated This property is deprecated and will be removed in v4.
|
||||
* This is for performance reasons. Use the `DocumentTitleContext` instead.
|
||||
* This is for performance reasons. Use the `DocumentTitleContext` instead
|
||||
* via the `useDocumentTitle` hook.
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useDocumentTitle } from '@payloadcms/ui'
|
||||
* const { title } = useDocumentTitle()
|
||||
* const { title } = useDocumentTitle()
|
||||
* ```
|
||||
*/
|
||||
title: string
|
||||
unlockDocument: (docID: number | string, slug: string) => Promise<void>
|
||||
unpublishedVersionCount: number
|
||||
updateDocumentEditor: (docID: number | string, slug: string, user: ClientUser) => Promise<void>
|
||||
/**
|
||||
* @deprecated This property is deprecated and will be removed in v4.
|
||||
* Use `setData` instead.
|
||||
*/
|
||||
updateSavedDocumentData: (data: Data) => void
|
||||
uploadStatus?: 'failed' | 'idle' | 'uploading'
|
||||
versionCount: number
|
||||
|
||||
@@ -19,8 +19,7 @@ export const useDocumentTitle = (): IDocumentTitleContext => use(DocumentTitleCo
|
||||
export const DocumentTitleProvider: React.FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
const { id, collectionSlug, docConfig, globalSlug, initialData, savedDocumentData } =
|
||||
useDocumentInfo()
|
||||
const { id, collectionSlug, data, docConfig, globalSlug, initialData } = useDocumentInfo()
|
||||
|
||||
const {
|
||||
config: {
|
||||
@@ -45,14 +44,14 @@ export const DocumentTitleProvider: React.FC<{
|
||||
setDocumentTitle(
|
||||
formatDocTitle({
|
||||
collectionConfig: collectionSlug ? (docConfig as ClientCollectionConfig) : undefined,
|
||||
data: { ...savedDocumentData, id },
|
||||
data: { ...data, id },
|
||||
dateFormat,
|
||||
fallback: id?.toString(),
|
||||
globalConfig: globalSlug ? (docConfig as ClientGlobalConfig) : undefined,
|
||||
i18n,
|
||||
}),
|
||||
)
|
||||
}, [savedDocumentData, dateFormat, i18n, id, collectionSlug, docConfig, globalSlug])
|
||||
}, [data, dateFormat, i18n, id, collectionSlug, docConfig, globalSlug])
|
||||
|
||||
return <DocumentTitleContext value={{ setDocumentTitle, title }}>{children}</DocumentTitleContext>
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export function DefaultEditView({
|
||||
BeforeFields,
|
||||
collectionSlug,
|
||||
currentEditor,
|
||||
data,
|
||||
disableActions,
|
||||
disableCreate,
|
||||
disableLeaveWithoutSaving,
|
||||
@@ -86,12 +87,12 @@ export function DefaultEditView({
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
redirectAfterRestore,
|
||||
savedDocumentData,
|
||||
setCurrentEditor,
|
||||
setData,
|
||||
setDocumentIsLocked,
|
||||
setLastUpdateTime,
|
||||
unlockDocument,
|
||||
updateDocumentEditor,
|
||||
updateSavedDocumentData,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const {
|
||||
@@ -237,7 +238,7 @@ export function DefaultEditView({
|
||||
setDocumentIsLocked(false)
|
||||
setCurrentEditor(null)
|
||||
} catch (err) {
|
||||
console.error('Failed to unlock before leave', err)
|
||||
console.error('Failed to unlock before leave', err) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,15 +257,17 @@ export function DefaultEditView({
|
||||
])
|
||||
|
||||
const onSave = useCallback(
|
||||
async (json): Promise<FormState> => {
|
||||
async (json, context?: Record<string, unknown>): Promise<FormState> => {
|
||||
const controller = handleAbortRef(abortOnSaveRef)
|
||||
|
||||
const document = json?.doc || json?.result
|
||||
|
||||
const updatedAt = document?.updatedAt || new Date().toISOString()
|
||||
|
||||
reportUpdate({
|
||||
id,
|
||||
entitySlug,
|
||||
updatedAt: document?.updatedAt || new Date().toISOString(),
|
||||
updatedAt,
|
||||
})
|
||||
|
||||
// If we're editing the doc of the logged-in user,
|
||||
@@ -273,10 +276,14 @@ export function DefaultEditView({
|
||||
void refreshCookieAsync()
|
||||
}
|
||||
|
||||
incrementVersionCount()
|
||||
setLastUpdateTime(updatedAt)
|
||||
|
||||
if (typeof updateSavedDocumentData === 'function') {
|
||||
void updateSavedDocumentData(document || {})
|
||||
if (context?.incrementVersionCount !== false) {
|
||||
incrementVersionCount()
|
||||
}
|
||||
|
||||
if (typeof setData === 'function') {
|
||||
void setData(document || {})
|
||||
}
|
||||
|
||||
if (typeof onSaveFromContext === 'function') {
|
||||
@@ -284,6 +291,7 @@ export function DefaultEditView({
|
||||
|
||||
void onSaveFromContext({
|
||||
...json,
|
||||
context,
|
||||
operation,
|
||||
updatedAt:
|
||||
operation === 'update'
|
||||
@@ -306,7 +314,7 @@ export function DefaultEditView({
|
||||
|
||||
await getDocPermissions(json)
|
||||
|
||||
if ((id || globalSlug) && !autosaveEnabled) {
|
||||
if (id || globalSlug) {
|
||||
const docPreferences = await getDocPreferences()
|
||||
|
||||
const { state } = await getFormState({
|
||||
@@ -341,18 +349,19 @@ export function DefaultEditView({
|
||||
user,
|
||||
collectionSlug,
|
||||
userSlug,
|
||||
incrementVersionCount,
|
||||
updateSavedDocumentData,
|
||||
setLastUpdateTime,
|
||||
setData,
|
||||
onSaveFromContext,
|
||||
redirectAfterCreate,
|
||||
isEditing,
|
||||
depth,
|
||||
redirectAfterCreate,
|
||||
getDocPermissions,
|
||||
globalSlug,
|
||||
autosaveEnabled,
|
||||
refreshCookieAsync,
|
||||
incrementVersionCount,
|
||||
adminRoute,
|
||||
locale,
|
||||
startRouteTransition,
|
||||
router,
|
||||
resetUploadEdits,
|
||||
getDocPreferences,
|
||||
@@ -362,7 +371,6 @@ export function DefaultEditView({
|
||||
schemaPathSegments,
|
||||
isLockingEnabled,
|
||||
setDocumentIsLocked,
|
||||
startRouteTransition,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -549,7 +557,7 @@ export function DefaultEditView({
|
||||
SaveButton,
|
||||
SaveDraftButton,
|
||||
}}
|
||||
data={savedDocumentData}
|
||||
data={data}
|
||||
disableActions={disableActions || isFolderCollection || isTrashed}
|
||||
disableCreate={disableCreate}
|
||||
EditMenuItems={EditMenuItems}
|
||||
@@ -612,14 +620,14 @@ export function DefaultEditView({
|
||||
className={`${baseClass}__auth`}
|
||||
collectionSlug={collectionConfig.slug}
|
||||
disableLocalStrategy={collectionConfig.auth?.disableLocalStrategy}
|
||||
email={savedDocumentData?.email}
|
||||
email={data?.email}
|
||||
loginWithUsername={auth?.loginWithUsername}
|
||||
operation={operation}
|
||||
readOnly={!hasSavePermission}
|
||||
requirePassword={!id}
|
||||
setValidateBeforeSubmit={setValidateBeforeSubmit}
|
||||
useAPIKey={auth.useAPIKey}
|
||||
username={savedDocumentData?.username}
|
||||
username={data?.username}
|
||||
verify={auth.verify}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user