Skip to content

Commit 4b302f2

Browse files
fix: incorrect formState after doc save (#9573)
### What? When the document is saved the formState was not being reset from the server. ### Why? getFormState was not being called onSuccess of the form submission ### How? The `Form` onSuccess function now allows for an optional return type of `FormState` if the functions returns formState then we check to see if that differs from the current formState on the client. If it does then we dispatch the `REPLACE_STATE` action with the newState. Fixes #9423
1 parent c7138b9 commit 4b302f2

File tree

11 files changed

+272
-102
lines changed

11 files changed

+272
-102
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClientCollectionConfig, Data, TypeWithID } from 'payload'
1+
import type { ClientCollectionConfig, Data, FormState, TypeWithID } from 'payload'
22

33
import { createContext, useContext } from 'react'
44

@@ -19,7 +19,7 @@ export type DocumentDrawerContextProps = {
1919
doc: TypeWithID
2020
operation: 'create' | 'update'
2121
result: Data
22-
}) => Promise<void> | void
22+
}) => Promise<FormState | void> | void
2323
}
2424

2525
export type DocumentDrawerContextType = DocumentDrawerContextProps

packages/ui/src/forms/Form/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,19 @@ export const Form: React.FC<FormProps> = (props) => {
323323
}
324324
if (res.status < 400) {
325325
if (typeof onSuccess === 'function') {
326-
await onSuccess(json)
326+
const newFormState = await onSuccess(json)
327+
if (newFormState) {
328+
const { newState: mergedFormState } = mergeServerFormState(
329+
contextRef.current.fields || {},
330+
newFormState,
331+
)
332+
333+
dispatchFields({
334+
type: 'REPLACE_STATE',
335+
optimize: false,
336+
state: mergedFormState,
337+
})
338+
}
327339
}
328340
setSubmitted(false)
329341
setProcessing(false)

packages/ui/src/forms/Form/mergeServerFormState.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export const mergeServerFormState = (
3737
/**
3838
* Handle error paths
3939
*/
40-
4140
const errorPathsResult = mergeErrorPaths(
4241
newFieldState.errorPaths,
4342
incomingState[path].errorPaths as unknown as string[],

packages/ui/src/forms/Form/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type FormProps = {
4141
log?: boolean
4242
onChange?: ((args: { formState: FormState }) => Promise<FormState>)[]
4343
onSubmit?: (fields: FormState, data: Data) => void
44-
onSuccess?: (json: unknown) => Promise<void> | void
44+
onSuccess?: (json: unknown) => Promise<FormState | void> | void
4545
redirect?: string
4646
submitted?: boolean
4747
uuid?: string

packages/ui/src/utilities/buildFormState.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ import { getClientSchemaMap } from './getClientSchemaMap.js'
1010
import { getSchemaMap } from './getSchemaMap.js'
1111
import { handleFormStateLocking } from './handleFormStateLocking.js'
1212

13+
export type LockedState = {
14+
isLocked: boolean
15+
lastEditedAt: string
16+
user: ClientUser | number | string
17+
}
18+
1319
type BuildFormStateSuccessResult = {
1420
clientConfig?: ClientConfig
1521
errors?: never
1622
indexPath?: string
17-
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser | number | string }
23+
lockedState?: LockedState
1824
state: FormState
1925
}
2026

packages/ui/src/views/Edit/index.tsx

Lines changed: 127 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
'use client'
22

3-
import { useRouter, useSearchParams } from 'next/navigation.js'
4-
import {
5-
type ClientCollectionConfig,
6-
type ClientGlobalConfig,
7-
type ClientSideEditViewProps,
8-
type ClientUser,
3+
import type {
4+
ClientCollectionConfig,
5+
ClientGlobalConfig,
6+
ClientSideEditViewProps,
7+
ClientUser,
8+
FormState,
99
} from 'payload'
10+
11+
import { useRouter, useSearchParams } from 'next/navigation.js'
1012
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
1113

1214
import type { FormProps } from '../../forms/Form/index.js'
15+
import type { LockedState } from '../../utilities/buildFormState.js'
1316

1417
import { DocumentControls } from '../../elements/DocumentControls/index.js'
1518
import { DocumentDrawerHeader } from '../../elements/DocumentDrawer/DrawerHeader/index.js'
@@ -34,9 +37,9 @@ import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
3437
import { handleGoBack } from '../../utilities/handleGoBack.js'
3538
import { handleTakeOver } from '../../utilities/handleTakeOver.js'
3639
import { Auth } from './Auth/index.js'
37-
import './index.scss'
3840
import { SetDocumentStepNav } from './SetDocumentStepNav/index.js'
3941
import { SetDocumentTitle } from './SetDocumentTitle/index.js'
42+
import './index.scss'
4043

4144
const baseClass = 'collection-edit'
4245

@@ -118,6 +121,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
118121
const { getFormState } = useServerFunctions()
119122

120123
const onChangeAbortControllerRef = useRef<AbortController>(null)
124+
const onSaveAbortControllerRef = useRef<AbortController>(null)
121125

122126
const locale = params.get('locale')
123127

@@ -139,15 +143,18 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
139143
const lockDurationInMilliseconds = lockDuration * 1000
140144

141145
let preventLeaveWithoutSaving = true
146+
let autosaveEnabled = false
142147

143148
if (collectionConfig) {
144-
preventLeaveWithoutSaving = !(
145-
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave
149+
autosaveEnabled = Boolean(
150+
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave,
146151
)
152+
preventLeaveWithoutSaving = !autosaveEnabled
147153
} else if (globalConfig) {
148-
preventLeaveWithoutSaving = !(
149-
globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave
154+
autosaveEnabled = Boolean(
155+
globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave,
150156
)
157+
preventLeaveWithoutSaving = !autosaveEnabled
151158
} else if (typeof disableLeaveWithoutSaving !== 'undefined') {
152159
preventLeaveWithoutSaving = !disableLeaveWithoutSaving
153160
}
@@ -197,8 +204,40 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
197204
return false
198205
})
199206

207+
const handleDocumentLocking = useCallback(
208+
(lockedState: LockedState) => {
209+
setDocumentIsLocked(true)
210+
const previousOwnerId =
211+
typeof documentLockStateRef.current?.user === 'object'
212+
? documentLockStateRef.current?.user?.id
213+
: documentLockStateRef.current?.user
214+
215+
if (lockedState) {
216+
const lockedUserID =
217+
typeof lockedState.user === 'string' || typeof lockedState.user === 'number'
218+
? lockedState.user
219+
: lockedState.user.id
220+
221+
if (!documentLockStateRef.current || lockedUserID !== previousOwnerId) {
222+
if (previousOwnerId === user.id && lockedUserID !== user.id) {
223+
setShowTakeOverModal(true)
224+
documentLockStateRef.current.hasShownLockedModal = true
225+
}
226+
227+
documentLockStateRef.current = {
228+
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
229+
isLocked: true,
230+
user: lockedState.user as ClientUser,
231+
}
232+
setCurrentEditor(lockedState.user as ClientUser)
233+
}
234+
}
235+
},
236+
[setCurrentEditor, setDocumentIsLocked, user.id],
237+
)
238+
200239
const onSave = useCallback(
201-
async (json) => {
240+
async (json): Promise<FormState> => {
202241
reportUpdate({
203242
id,
204243
entitySlug,
@@ -224,11 +263,6 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
224263
})
225264
}
226265

227-
// Unlock the document after save
228-
if ((id || globalSlug) && isLockingEnabled) {
229-
setDocumentIsLocked(false)
230-
}
231-
232266
if (!isEditing && depth < 2) {
233267
// Redirect to the same locale if it's been set
234268
const redirectRoute = formatAdminURL({
@@ -241,28 +275,63 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
241275
}
242276

243277
await getDocPermissions(json)
278+
279+
if ((id || globalSlug) && !autosaveEnabled) {
280+
abortAndIgnore(onSaveAbortControllerRef.current)
281+
const controller = new AbortController()
282+
onSaveAbortControllerRef.current = controller
283+
284+
const docPreferences = await getDocPreferences()
285+
286+
const { state } = await getFormState({
287+
id,
288+
collectionSlug,
289+
data: json?.doc,
290+
docPermissions,
291+
docPreferences,
292+
globalSlug,
293+
operation,
294+
renderAllFields: true,
295+
returnLockStatus: false,
296+
schemaPath: schemaPathSegments.join('.'),
297+
signal: controller.signal,
298+
})
299+
300+
// Unlock the document after save
301+
if (isLockingEnabled) {
302+
setDocumentIsLocked(false)
303+
}
304+
305+
return state
306+
}
244307
},
245308
[
246-
updateSavedDocumentData,
247-
reportUpdate,
248-
id,
249-
entitySlug,
250-
user,
309+
adminRoute,
251310
collectionSlug,
252-
userSlug,
253-
incrementVersionCount,
254-
onSaveFromContext,
255-
globalSlug,
256-
isLockingEnabled,
257-
isEditing,
258311
depth,
312+
docPermissions,
313+
entitySlug,
259314
getDocPermissions,
260-
refreshCookieAsync,
261-
setDocumentIsLocked,
262-
adminRoute,
315+
getDocPreferences,
316+
getFormState,
317+
globalSlug,
318+
id,
319+
incrementVersionCount,
320+
isEditing,
321+
isLockingEnabled,
263322
locale,
264-
router,
323+
onSaveFromContext,
324+
operation,
325+
refreshCookieAsync,
326+
reportUpdate,
265327
resetUploadEdits,
328+
router,
329+
schemaPathSegments,
330+
setDocumentIsLocked,
331+
updateSavedDocumentData,
332+
user,
333+
userSlug,
334+
autosaveEnabled,
266335
],
267336
)
268337

@@ -293,92 +362,54 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
293362
globalSlug,
294363
operation,
295364
// Performance optimization: Setting it to false ensure that only fields that have explicit requireRender set in the form state will be rendered (e.g. new array rows).
296-
// We only wanna render ALL fields on initial render, not in onChange.
365+
// We only want to render ALL fields on initial render, not in onChange.
297366
renderAllFields: false,
298-
returnLockStatus: isLockingEnabled ? true : false,
367+
returnLockStatus: isLockingEnabled,
299368
schemaPath: schemaPathSegments.join('.'),
300369
signal: controller.signal,
301370
updateLastEdited,
302371
})
303372

304-
setDocumentIsLocked(true)
305-
306373
if (isLockingEnabled) {
307-
const previousOwnerId =
308-
typeof documentLockStateRef.current?.user === 'object'
309-
? documentLockStateRef.current?.user?.id
310-
: documentLockStateRef.current?.user
311-
312-
if (lockedState) {
313-
const lockedUserID =
314-
typeof lockedState.user === 'string' || typeof lockedState.user === 'number'
315-
? lockedState.user
316-
: lockedState.user.id
317-
318-
if (!documentLockStateRef.current || lockedUserID !== previousOwnerId) {
319-
if (previousOwnerId === user.id && lockedUserID !== user.id) {
320-
setShowTakeOverModal(true)
321-
documentLockStateRef.current.hasShownLockedModal = true
322-
}
323-
324-
documentLockStateRef.current = documentLockStateRef.current = {
325-
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
326-
isLocked: true,
327-
user: lockedState.user as ClientUser,
328-
}
329-
setCurrentEditor(lockedState.user as ClientUser)
330-
}
331-
}
374+
handleDocumentLocking(lockedState)
332375
}
333376

334377
return state
335378
},
336379
[
337-
editSessionStartTime,
338-
isLockingEnabled,
339-
getDocPreferences,
340-
getFormState,
341380
id,
342381
collectionSlug,
343-
docPermissions,
382+
getDocPreferences,
383+
getFormState,
344384
globalSlug,
385+
handleDocumentLocking,
386+
isLockingEnabled,
345387
operation,
346388
schemaPathSegments,
347-
setDocumentIsLocked,
348-
user.id,
349-
setCurrentEditor,
389+
docPermissions,
390+
editSessionStartTime,
350391
],
351392
)
352393

353394
// Clean up when the component unmounts or when the document is unlocked
354395
useEffect(() => {
355396
return () => {
356-
if (!isLockingEnabled) {
357-
return
358-
}
359-
360-
const currentPath = window.location.pathname
361-
362-
const documentId = id || globalSlug
363-
364-
// Routes where we do NOT want to unlock the document
365-
const stayWithinDocumentPaths = ['preview', 'api', 'versions']
366-
367-
const isStayingWithinDocument = stayWithinDocumentPaths.some((path) =>
368-
currentPath.includes(path),
369-
)
370-
371-
// Unlock the document only if we're actually navigating away from the document
372-
if (documentId && documentIsLocked && !isStayingWithinDocument) {
373-
// Check if this user is still the current editor
374-
if (
375-
typeof documentLockStateRef.current?.user === 'object'
376-
? documentLockStateRef.current?.user?.id === user?.id
377-
: documentLockStateRef.current?.user === user?.id
378-
) {
379-
void unlockDocument(id, collectionSlug ?? globalSlug)
380-
setDocumentIsLocked(false)
381-
setCurrentEditor(null)
397+
if (isLockingEnabled && documentIsLocked && (id || globalSlug)) {
398+
// Only retain the lock if the user is still viewing the document
399+
const shouldUnlockDocument = !['preview', 'api', 'versions'].some((path) =>
400+
window.location.pathname.includes(path),
401+
)
402+
if (shouldUnlockDocument) {
403+
// Check if this user is still the current editor
404+
if (
405+
typeof documentLockStateRef.current?.user === 'object'
406+
? documentLockStateRef.current?.user?.id === user?.id
407+
: documentLockStateRef.current?.user === user?.id
408+
) {
409+
void unlockDocument(id, collectionSlug ?? globalSlug)
410+
setDocumentIsLocked(false)
411+
setCurrentEditor(null)
412+
}
382413
}
383414
}
384415

@@ -399,6 +430,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
399430
useEffect(() => {
400431
return () => {
401432
abortAndIgnore(onChangeAbortControllerRef.current)
433+
abortAndIgnore(onSaveAbortControllerRef.current)
402434
}
403435
}, [])
404436

test/hooks/collectionSlugs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const beforeValidateSlug = 'before-validate'

0 commit comments

Comments
 (0)