Skip to content

Commit 1d81b0c

Browse files
authored
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
1 parent 9c8f320 commit 1d81b0c

File tree

14 files changed

+217
-204
lines changed

14 files changed

+217
-204
lines changed

docs/admin/react-hooks.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ The `useDocumentInfo` hook provides information about the current document being
739739
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
740740
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
741741
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
742-
| **`savedDocumentData`** | The saved data of the document. |
742+
| **`data`** | The saved data of the document. |
743743
| **`setDocFieldPreferences`** | Method to set preferences for a specific field. [More details](./preferences). |
744744
| **`setDocumentTitle`** | Method to set the document title. |
745745
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |

packages/ui/src/elements/Autosave/index.tsx

Lines changed: 32 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
55
import { dequal } from 'dequal/lite'
66
import { reduceFieldsToValues, versionDefaults } from 'payload/shared'
77
import React, { useDeferredValue, useEffect, useRef, useState } from 'react'
8-
import { toast } from 'sonner'
98

109
import {
1110
useAllFormFields,
@@ -17,13 +16,11 @@ import { useDebounce } from '../../hooks/useDebounce.js'
1716
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
1817
import { useQueues } from '../../hooks/useQueues.js'
1918
import { useConfig } from '../../providers/Config/index.js'
20-
import { useDocumentEvents } from '../../providers/DocumentEvents/index.js'
2119
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
2220
import { useLocale } from '../../providers/Locale/index.js'
2321
import { useTranslation } from '../../providers/Translation/index.js'
2422
import { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js'
2523
import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFieldsToValuesWithValidation.js'
26-
import { useDocumentDrawerContext } from '../DocumentDrawer/Provider.js'
2724
import { LeaveWithoutSaving } from '../LeaveWithoutSaving/index.js'
2825
import './index.scss'
2926

@@ -51,16 +48,11 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
5148
incrementVersionCount,
5249
lastUpdateTime,
5350
mostRecentVersionIsAutosaved,
54-
setLastUpdateTime,
5551
setMostRecentVersionIsAutosaved,
5652
setUnpublishedVersionCount,
57-
updateSavedDocumentData,
5853
} = useDocumentInfo()
5954

60-
const { onSave: onSaveFromDocumentDrawer } = useDocumentDrawerContext()
61-
62-
const { reportUpdate } = useDocumentEvents()
63-
const { dispatchFields, isValid, setBackgroundProcessing, setIsValid } = useForm()
55+
const { isValid, setBackgroundProcessing, submit } = useForm()
6456

6557
const [formState] = useAllFormFields()
6658
const modified = useFormModified()
@@ -151,118 +143,38 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
151143
method = 'POST'
152144
}
153145

154-
if (url) {
155-
if (modifiedRef.current) {
156-
const { data, valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true)
157-
158-
data._status = 'draft'
159-
160-
const skipSubmission =
161-
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
162-
163-
if (!skipSubmission) {
164-
let res
165-
166-
try {
167-
res = await fetch(url, {
168-
body: JSON.stringify(data),
169-
credentials: 'include',
170-
headers: {
171-
'Accept-Language': i18n.language,
172-
'Content-Type': 'application/json',
173-
},
174-
method,
175-
})
176-
} catch (_err) {
177-
// Swallow Error
178-
}
179-
180-
const newDate = new Date()
181-
// We need to log the time in order to figure out if we need to trigger the state off later
182-
endTimestamp = newDate.getTime()
183-
184-
const json = await res.json()
185-
186-
if (res.status === 200) {
187-
setLastUpdateTime(newDate.getTime())
188-
189-
reportUpdate({
190-
id,
191-
entitySlug,
192-
updatedAt: newDate.toISOString(),
193-
})
194-
195-
// if onSaveFromDocumentDrawer is defined, call it
196-
if (typeof onSaveFromDocumentDrawer === 'function') {
197-
void onSaveFromDocumentDrawer({
198-
...json,
199-
operation: 'update',
200-
})
201-
}
202-
203-
if (!mostRecentVersionIsAutosaved) {
204-
incrementVersionCount()
205-
setMostRecentVersionIsAutosaved(true)
206-
setUnpublishedVersionCount((prev) => prev + 1)
207-
}
208-
}
209-
210-
if (versionsConfig?.drafts && versionsConfig?.drafts?.validate && json?.errors) {
211-
if (Array.isArray(json.errors)) {
212-
const [fieldErrors, nonFieldErrors] = json.errors.reduce(
213-
([fieldErrs, nonFieldErrs], err) => {
214-
const newFieldErrs = []
215-
const newNonFieldErrs = []
216-
217-
if (err?.message) {
218-
newNonFieldErrs.push(err)
219-
}
220-
221-
if (Array.isArray(err?.data)) {
222-
err.data.forEach((dataError) => {
223-
if (dataError?.field) {
224-
newFieldErrs.push(dataError)
225-
} else {
226-
newNonFieldErrs.push(dataError)
227-
}
228-
})
229-
}
230-
231-
return [
232-
[...fieldErrs, ...newFieldErrs],
233-
[...nonFieldErrs, ...newNonFieldErrs],
234-
]
235-
},
236-
[[], []],
237-
)
238-
239-
dispatchFields({
240-
type: 'ADD_SERVER_ERRORS',
241-
errors: fieldErrors,
242-
})
243-
244-
nonFieldErrors.forEach((err) => {
245-
toast.error(err.message || i18n.t('error:unknown'))
246-
})
247-
248-
setIsValid(false)
249-
hideIndicator()
250-
return
251-
}
252-
} else {
253-
// If it's not an error then we can update the document data inside the context
254-
const document = json?.doc || json?.result
255-
256-
// Manually update the data since this function doesn't fire the `submit` function from useForm
257-
if (document) {
258-
setIsValid(true)
259-
updateSavedDocumentData(document)
260-
}
261-
}
262-
263-
hideIndicator()
264-
}
146+
const { valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true)
147+
148+
const skipSubmission =
149+
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
150+
151+
if (!skipSubmission && modifiedRef.current && url) {
152+
const result = await submit({
153+
action: url,
154+
context: {
155+
incrementVersionCount: false,
156+
},
157+
disableFormWhileProcessing: false,
158+
disableSuccessStatus: true,
159+
method,
160+
overrides: {
161+
_status: 'draft',
162+
},
163+
skipValidation: versionsConfig?.drafts && !versionsConfig?.drafts?.validate,
164+
})
165+
166+
if (result && result?.res?.ok && !mostRecentVersionIsAutosaved) {
167+
incrementVersionCount()
168+
setMostRecentVersionIsAutosaved(true)
169+
setUnpublishedVersionCount((prev) => prev + 1)
265170
}
171+
172+
const newDate = new Date()
173+
174+
// We need to log the time in order to figure out if we need to trigger the state off later
175+
endTimestamp = newDate.getTime()
176+
177+
hideIndicator()
266178
}
267179
}
268180
},

packages/ui/src/elements/DocumentDrawer/Provider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export type DocumentDrawerContextProps = {
2020
}) => Promise<void> | void
2121
readonly onSave?: (args: {
2222
collectionConfig?: ClientCollectionConfig
23+
/**
24+
* @experimental - Note: this property is experimental and may change in the future. Use as your own discretion.
25+
* If you want to pass additional data to the onSuccess callback, you can use this context object.
26+
*/
27+
context?: Record<string, unknown>
2328
doc: TypeWithID
2429
operation: 'create' | 'update'
2530
result: Data

packages/ui/src/elements/Upload/index.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
161161

162162
const { t } = useTranslation()
163163
const { setModified } = useForm()
164-
const { id, docPermissions, savedDocumentData, setUploadStatus } = useDocumentInfo()
164+
const { id, data, docPermissions, setUploadStatus } = useDocumentInfo()
165165
const isFormSubmitting = useFormProcessing()
166166
const { errorMessage, setValue, showError, value } = useField<File>({
167167
path: 'file',
@@ -349,7 +349,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
349349

350350
const acceptMimeTypes = uploadConfig.mimeTypes?.join(', ')
351351

352-
const imageCacheTag = uploadConfig?.cacheTags && savedDocumentData?.updatedAt
352+
const imageCacheTag = uploadConfig?.cacheTags && data?.updatedAt
353353

354354
useEffect(() => {
355355
const handleControlFileUrl = async () => {
@@ -375,11 +375,11 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
375375
return (
376376
<div className={[fieldBaseClass, baseClass].filter(Boolean).join(' ')}>
377377
<FieldError message={errorMessage} showError={showError} />
378-
{savedDocumentData && savedDocumentData.filename && !removedFile && (
378+
{data && data.filename && !removedFile && (
379379
<FileDetails
380380
collectionSlug={collectionSlug}
381381
customUploadActions={customActions}
382-
doc={savedDocumentData}
382+
doc={data}
383383
enableAdjustments={showCrop || showFocalPoint}
384384
handleRemove={canRemoveUpload ? handleFileRemoval : undefined}
385385
hasImageSizes={hasImageSizes}
@@ -388,7 +388,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
388388
uploadConfig={uploadConfig}
389389
/>
390390
)}
391-
{((!uploadConfig.hideFileInputOnCreate && !savedDocumentData?.filename) || removedFile) && (
391+
{((!uploadConfig.hideFileInputOnCreate && !data?.filename) || removedFile) && (
392392
<div className={`${baseClass}__upload`}>
393393
{!value && !showUrlInput && (
394394
<Dropzone onChange={handleFileSelection}>
@@ -506,7 +506,7 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
506506
<UploadActions
507507
customActions={customActions}
508508
enableAdjustments={showCrop || showFocalPoint}
509-
enablePreviewSizes={hasImageSizes && savedDocumentData?.filename && !removedFile}
509+
enablePreviewSizes={hasImageSizes && data?.filename && !removedFile}
510510
mimeType={value.type}
511511
/>
512512
</div>
@@ -523,17 +523,17 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
523523
)}
524524
</div>
525525
)}
526-
{(value || savedDocumentData?.filename) && (
526+
{(value || data?.filename) && (
527527
<EditDepthProvider>
528528
<Drawer Header={null} slug={editDrawerSlug}>
529529
<EditUpload
530-
fileName={value?.name || savedDocumentData?.filename}
531-
fileSrc={savedDocumentData?.url || fileSrc}
530+
fileName={value?.name || data?.filename}
531+
fileSrc={data?.url || fileSrc}
532532
imageCacheTag={imageCacheTag}
533533
initialCrop={uploadEdits?.crop ?? undefined}
534534
initialFocalPoint={{
535-
x: uploadEdits?.focalPoint?.x || savedDocumentData?.focalX || 50,
536-
y: uploadEdits?.focalPoint?.y || savedDocumentData?.focalY || 50,
535+
x: uploadEdits?.focalPoint?.x || data?.focalX || 50,
536+
y: uploadEdits?.focalPoint?.y || data?.focalY || 50,
537537
}}
538538
onSave={onEditsSave}
539539
showCrop={showCrop}
@@ -542,18 +542,14 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
542542
</Drawer>
543543
</EditDepthProvider>
544544
)}
545-
{savedDocumentData && hasImageSizes && (
545+
{data && hasImageSizes && (
546546
<Drawer
547547
className={`${baseClass}__previewDrawer`}
548548
hoverTitle
549549
slug={sizePreviewSlug}
550-
title={t('upload:sizesFor', { label: savedDocumentData.filename })}
550+
title={t('upload:sizesFor', { label: data.filename })}
551551
>
552-
<PreviewSizes
553-
doc={savedDocumentData}
554-
imageCacheTag={imageCacheTag}
555-
uploadConfig={uploadConfig}
556-
/>
552+
<PreviewSizes doc={data} imageCacheTag={imageCacheTag} uploadConfig={uploadConfig} />
557553
</Drawer>
558554
)}
559555
</div>

0 commit comments

Comments
 (0)