fix(richtext-lexical): inline blocks did not store nested fields correctly (#10578)

Fixes https://github.com/payloadcms/payload/issues/10555

Form state with nested fields was not unflattened before saving field
data to the node
This commit is contained in:
Alessio Gravili
2025-01-14 14:17:25 -07:00
committed by GitHub
parent 05b03b2dcd
commit 61117ee5cb
29 changed files with 461 additions and 505 deletions

View File

@@ -491,14 +491,13 @@ export const BlockComponent: React.FC<Props> = (props) => {
fields={clientBlock?.fields}
initialState={initialState}
onChange={[onChange]}
onSubmit={(formState) => {
onSubmit={(formState, newData) => {
// This is only called when form is submitted from drawer - usually only the case if the block has a custom Block component
const newData: any = reduceFieldsToValues(formState)
newData.blockType = formData.blockType
editor.update(() => {
const node = $getNodeByKey(nodeKey)
if (node && $isBlockNode(node)) {
node.setFields(newData, true)
node.setFields(newData as BlockFields, true)
}
})
toggleDrawer()

View File

@@ -3,7 +3,7 @@
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react'
const baseClass = 'inline-block'
import type { BlocksFieldClient, FormState } from 'payload'
import type { BlocksFieldClient, Data, FormState } from 'payload'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
@@ -33,7 +33,6 @@ import {
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
} from 'lexical'
import { reduceFieldsToValues } from 'payload/shared'
import './index.scss'
@@ -308,13 +307,13 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
* HANDLE FORM SUBMIT
*/
const onFormSubmit = useCallback(
(formState: FormState) => {
const newData: any = reduceFieldsToValues(formState)
(formState: FormState, newData: Data) => {
newData.blockType = formData.blockType
editor.update(() => {
const node = $getNodeByKey(nodeKey)
if (node && $isInlineBlockNode(node)) {
node.setFields(newData, true)
node.setFields(newData as InlineBlockFields, true)
}
})
},
@@ -414,8 +413,8 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
fields={clientBlock?.fields}
initialState={initialState || {}}
onChange={[onChange]}
onSubmit={(formState) => {
onFormSubmit(formState)
onSubmit={(formState, data) => {
onFormSubmit(formState, data)
toggleDrawer()
}}
uuid={uuid()}

View File

@@ -36,7 +36,7 @@ describe('Array', () => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
}))

View File

@@ -32,7 +32,7 @@ describe('Collapsibles', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -45,7 +45,7 @@ describe('Conditional Logic', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -35,7 +35,7 @@ describe('Custom IDs', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -36,7 +36,7 @@ describe('Date', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -35,7 +35,7 @@ describe('Email', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -32,7 +32,7 @@ describe('Radio', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -36,7 +36,7 @@ describe('JSON', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@ describe('lexicalMain', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
context = await browser.newContext()
page = await context.newPage()

View File

@@ -176,6 +176,25 @@ const editorConfig: ServerEditorConfig = {
},
],
inlineBlocks: [
{
slug: 'AvatarGroup',
interfaceName: 'AvatarGroupBlock',
fields: [
{
name: 'avatars',
type: 'array',
minRows: 1,
maxRows: 6,
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'uploads',
},
],
},
],
},
{
slug: 'myInlineBlock',
admin: {

View File

@@ -38,7 +38,7 @@ describe('Number', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -37,7 +37,7 @@ describe('Point', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -31,7 +31,7 @@ describe('Radio', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -42,7 +42,7 @@ describe('relationship', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
}))

View File

@@ -31,7 +31,7 @@ describe('Rich Text', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ serverURL } = await initPayloadE2ENoConfig({
;({ serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
}))

View File

@@ -32,7 +32,7 @@ describe('Row', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -35,7 +35,7 @@ describe('Radio', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -39,7 +39,7 @@ describe('Tabs', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -36,7 +36,7 @@ describe('Tabs', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -43,7 +43,7 @@ describe('Text', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -31,7 +31,7 @@ describe('Radio', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -38,7 +38,7 @@ describe('Upload', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -33,7 +33,7 @@ describe('Upload with restrictions', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig({
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))

View File

@@ -3374,6 +3374,21 @@ export interface LexicalBlocksRadioButtonsBlock {
blockName?: string | null;
blockType: 'radioButtons';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "AvatarGroupBlock".
*/
export interface AvatarGroupBlock {
avatars?:
| {
image?: (string | null) | Upload;
id?: string | null;
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'AvatarGroup';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".

View File

@@ -39,7 +39,7 @@ let serverURL: string
describe('Locked Documents', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
globalUrl = new AdminUrlUtil(serverURL, 'menu')
postsUrl = new AdminUrlUtil(serverURL, 'posts')

View File

@@ -8,7 +8,7 @@ const dirname = path.dirname(filename)
dotenv.config({ path: path.resolve(dirname, 'test.env') })
let multiplier = process.env.CI ? 3 : 0.5
let multiplier = process.env.CI ? 3 : 0.75
let smallMultiplier = process.env.CI ? 2 : 0.75
export const TEST_TIMEOUT_LONG = 640000 * multiplier // 8*3 minutes - used as timeOut for the beforeAll