fix: set initialValues alongside values during onSuccess (#10825)
### What? Initial values should be set from the server when `acceptValues` is true. ### Why? This is needed since we take the values from the server after a successful form submission. ### How? Add `initialValue` into `serverPropsToAccept` when `acceptValues` is true. Fixes https://github.com/payloadcms/payload/issues/10820 --------- Co-authored-by: Alessio Gravili <alessio@gravili.de>
This commit is contained in:
@@ -87,8 +87,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const { getFormState } = useServerFunctions()
|
||||
const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_blocks.${formData.blockType}.fields`
|
||||
|
||||
const [initialState, setInitialState] = React.useState<false | FormState | undefined>(
|
||||
initialLexicalFormState?.[formData.id]?.formState
|
||||
const [initialState, setInitialState] = React.useState<false | FormState | undefined>(() => {
|
||||
return initialLexicalFormState?.[formData.id]?.formState
|
||||
? {
|
||||
...initialLexicalFormState?.[formData.id]?.formState,
|
||||
blockName: {
|
||||
@@ -98,11 +98,20 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
value: formData.blockName,
|
||||
},
|
||||
}
|
||||
: false,
|
||||
)
|
||||
: false
|
||||
})
|
||||
|
||||
const hasMounted = useRef(false)
|
||||
const prevCacheBuster = useRef(cacheBuster)
|
||||
useEffect(() => {
|
||||
setInitialState(false)
|
||||
if (hasMounted.current) {
|
||||
if (prevCacheBuster.current !== cacheBuster) {
|
||||
setInitialState(false)
|
||||
}
|
||||
prevCacheBuster.current = cacheBuster
|
||||
} else {
|
||||
hasMounted.current = true
|
||||
}
|
||||
}, [cacheBuster])
|
||||
|
||||
const [CustomLabel, setCustomLabel] = React.useState<React.ReactNode | undefined>(
|
||||
@@ -148,6 +157,22 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
value: formData.blockName,
|
||||
}
|
||||
|
||||
const newFormStateData: BlockFields = reduceFieldsToValues(
|
||||
deepCopyObjectSimpleWithoutReactComponents(state),
|
||||
true,
|
||||
) as BlockFields
|
||||
|
||||
// Things like default values may come back from the server => update the node with the new data
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
if (node && $isBlockNode(node)) {
|
||||
const newData = newFormStateData
|
||||
newData.blockType = formData.blockType
|
||||
|
||||
node.setFields(newData, true)
|
||||
}
|
||||
})
|
||||
|
||||
setInitialState(state)
|
||||
setCustomLabel(state._components?.customComponents?.BlockLabel)
|
||||
setCustomBlock(state._components?.customComponents?.Block)
|
||||
@@ -166,6 +191,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
schemaFieldsPath,
|
||||
id,
|
||||
formData,
|
||||
editor,
|
||||
nodeKey,
|
||||
initialState,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { $getNodeByKey } from 'lexical'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { deepCopyObjectSimpleWithoutReactComponents } from 'payload/shared'
|
||||
import { deepCopyObjectSimpleWithoutReactComponents, reduceFieldsToValues } from 'payload/shared'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { InlineBlockFields } from '../../server/nodes/InlineBlocksNode.js'
|
||||
@@ -86,11 +86,20 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
const firstTimeDrawer = useRef(false)
|
||||
|
||||
const [initialState, setInitialState] = React.useState<false | FormState | undefined>(
|
||||
initialLexicalFormState?.[formData.id]?.formState,
|
||||
() => initialLexicalFormState?.[formData.id]?.formState,
|
||||
)
|
||||
|
||||
const hasMounted = useRef(false)
|
||||
const prevCacheBuster = useRef(cacheBuster)
|
||||
useEffect(() => {
|
||||
setInitialState(false)
|
||||
if (hasMounted.current) {
|
||||
if (prevCacheBuster.current !== cacheBuster) {
|
||||
setInitialState(false)
|
||||
}
|
||||
prevCacheBuster.current = cacheBuster
|
||||
} else {
|
||||
hasMounted.current = true
|
||||
}
|
||||
}, [cacheBuster])
|
||||
|
||||
const [CustomLabel, setCustomLabel] = React.useState<React.ReactNode | undefined>(
|
||||
@@ -176,6 +185,22 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
})
|
||||
|
||||
if (state) {
|
||||
const newFormStateData: InlineBlockFields = reduceFieldsToValues(
|
||||
deepCopyObjectSimpleWithoutReactComponents(state),
|
||||
true,
|
||||
) as InlineBlockFields
|
||||
|
||||
// Things like default values may come back from the server => update the node with the new data
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey)
|
||||
if (node && $isInlineBlockNode(node)) {
|
||||
const newData = newFormStateData
|
||||
newData.blockType = formData.blockType
|
||||
|
||||
node.setFields(newData, true)
|
||||
}
|
||||
})
|
||||
|
||||
setInitialState(state)
|
||||
setCustomLabel(state['_components']?.customComponents?.BlockLabel)
|
||||
setCustomBlock(state['_components']?.customComponents?.Block)
|
||||
@@ -191,6 +216,8 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
}
|
||||
}, [
|
||||
getFormState,
|
||||
editor,
|
||||
nodeKey,
|
||||
schemaFieldsPath,
|
||||
id,
|
||||
formData,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
FieldLabel,
|
||||
RenderCustomComponent,
|
||||
useEditDepth,
|
||||
useEffectEvent,
|
||||
useField,
|
||||
} from '@payloadcms/ui'
|
||||
import { mergeFieldStyles } from '@payloadcms/ui/shared'
|
||||
@@ -15,11 +16,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
import type { SanitizedClientEditorConfig } from '../lexical/config/types.js'
|
||||
import type { LexicalRichTextFieldProps } from '../types.js'
|
||||
|
||||
import '../lexical/theme/EditorTheme.scss'
|
||||
import './bundled.css'
|
||||
import './index.scss'
|
||||
|
||||
import type { LexicalRichTextFieldProps } from '../types.js'
|
||||
|
||||
import { LexicalProvider } from '../lexical/LexicalProvider.js'
|
||||
|
||||
const baseClass = 'rich-text-lexical'
|
||||
@@ -126,14 +129,30 @@ const RichTextComponent: React.FC<
|
||||
|
||||
const styles = useMemo(() => mergeFieldStyles(field), [field])
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(initialValue) !== JSON.stringify(prevInitialValueRef.current)) {
|
||||
prevInitialValueRef.current = initialValue
|
||||
if (JSON.stringify(prevValueRef.current) !== JSON.stringify(value)) {
|
||||
const handleInitialValueChange = useEffectEvent(
|
||||
(initialValue: SerializedEditorState | undefined) => {
|
||||
// Object deep equality check here, as re-mounting the editor if
|
||||
// the new value is the same as the old one is not necessary
|
||||
if (
|
||||
prevValueRef.current !== value &&
|
||||
JSON.stringify(prevValueRef.current) !== JSON.stringify(value)
|
||||
) {
|
||||
prevInitialValueRef.current = initialValue
|
||||
prevValueRef.current = value
|
||||
setRerenderProviderKey(new Date())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Needs to trigger for object reference changes - otherwise,
|
||||
// reacting to the same initial value change twice will cause
|
||||
// the second change to be ignored, even though the value has changed.
|
||||
// That's because initialValue is not kept up-to-date
|
||||
if (!Object.is(initialValue, prevInitialValueRef.current)) {
|
||||
handleInitialValueChange(initialValue)
|
||||
}
|
||||
}, [initialValue, value])
|
||||
}, [initialValue])
|
||||
|
||||
return (
|
||||
<div className={classes} key={pathWithEditDepth} style={styles}>
|
||||
|
||||
@@ -18,6 +18,8 @@ export { useIntersect } from '../../hooks/useIntersect.js'
|
||||
export { usePayloadAPI } from '../../hooks/usePayloadAPI.js'
|
||||
export { useResize } from '../../hooks/useResize.js'
|
||||
export { useThrottledEffect } from '../../hooks/useThrottledEffect.js'
|
||||
export { useEffectEvent } from '../../hooks/useEffectEvent.js'
|
||||
|
||||
export { useUseTitleField } from '../../hooks/useUseAsTitle.js'
|
||||
|
||||
// elements
|
||||
|
||||
@@ -38,6 +38,7 @@ export const mergeServerFormState = ({
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
serverPropsToAccept.push('initialValue')
|
||||
}
|
||||
|
||||
for (const [path, newFieldState] of Object.entries(existingState)) {
|
||||
|
||||
@@ -47,7 +47,6 @@ export const FilterOptionsBlock: Block = {
|
||||
type: 'relationship',
|
||||
relationTo: 'text-fields',
|
||||
filterOptions: ({ siblingData }) => {
|
||||
console.log('SD', siblingData)
|
||||
// @ts-expect-error
|
||||
if (!siblingData?.groupText) {
|
||||
return true
|
||||
|
||||
61
test/fields/collections/Lexical/components/ClearState.tsx
Normal file
61
test/fields/collections/Lexical/components/ClearState.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import type { SerializedParagraphNode, SerializedTextNode } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { useForm } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const ClearState = ({ fieldName }: { fieldName: string }) => {
|
||||
const { dispatchFields, fields } = useForm()
|
||||
|
||||
const clearState = React.useCallback(() => {
|
||||
const newState = {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '',
|
||||
version: 1,
|
||||
} as SerializedTextNode,
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
version: 1,
|
||||
} as SerializedParagraphNode,
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
}
|
||||
dispatchFields({
|
||||
type: 'REPLACE_STATE',
|
||||
state: {
|
||||
...fields,
|
||||
[fieldName]: {
|
||||
...fields[fieldName],
|
||||
initialValue: newState,
|
||||
value: newState,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [dispatchFields, fields, fieldName])
|
||||
|
||||
return (
|
||||
<button id={`clear-lexical-${fieldName}`} onClick={clearState} type="button">
|
||||
Clear State
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -269,6 +269,40 @@ describe('lexicalMain', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('should be able to externally mutate editor state', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(1).locator('.editor-scroller') // first
|
||||
await expect(richTextField).toBeVisible()
|
||||
await richTextField.click() // Use click, because focus does not work
|
||||
await page.keyboard.type('some text')
|
||||
const spanInEditor = richTextField.locator('span').first()
|
||||
await expect(spanInEditor).toHaveText('some text')
|
||||
await saveDocAndAssert(page)
|
||||
await page.locator('#clear-lexical-lexicalSimple').click()
|
||||
await expect(spanInEditor).not.toBeAttached()
|
||||
})
|
||||
|
||||
// This test ensures that the second state clear change is respected too, even though
|
||||
// initialValue is stale and equal to the previous state change result value-wise
|
||||
test('should be able to externally mutate editor state twice', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(1).locator('.editor-scroller') // first
|
||||
await expect(richTextField).toBeVisible()
|
||||
await richTextField.click() // Use click, because focus does not work
|
||||
await page.keyboard.type('some text')
|
||||
const spanInEditor = richTextField.locator('span').first()
|
||||
await expect(spanInEditor).toHaveText('some text')
|
||||
await saveDocAndAssert(page)
|
||||
await page.locator('#clear-lexical-lexicalSimple').click()
|
||||
await expect(spanInEditor).not.toBeAttached()
|
||||
|
||||
await richTextField.click()
|
||||
await page.keyboard.type('some text')
|
||||
await expect(spanInEditor).toHaveText('some text')
|
||||
await page.locator('#clear-lexical-lexicalSimple').click()
|
||||
await expect(spanInEditor).not.toBeAttached()
|
||||
})
|
||||
|
||||
test('should be able to bold text using floating select toolbar', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
|
||||
|
||||
@@ -321,6 +321,20 @@ export const LexicalFields: CollectionConfig = {
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'clearLexicalState',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/collections/Lexical/components/ClearState.js#ClearState',
|
||||
clientProps: {
|
||||
fieldName: 'lexicalSimple',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lexicalWithBlocks',
|
||||
type: 'richText',
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/admin/config.ts"],
|
||||
"@payload-config": ["./test/_community/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user