fix(richtext-lexical): editor re-mounting on save due to json key order not being preserved in postgres (#13962)
Fixes https://github.com/payloadcms/payload/issues/13904 When using Postgres, saving a document can cause the editor to re-mount. If the document includes a blocks field, this leads to the editor incorrectly resetting its value to the previous state. The issue occurs because the editor is re-mounted without recalculating the initial Lexical state. This re-mounting behavior is caused by how Postgres handles JSON storage: - With `jsonb` columns, unlike `json` columns, object key order is not guaranteed. - As a result, saving and reloading the Lexical editor state shifts key order, causing `JSON.stringify()` comparisons to fail. ## Solution To fix this, we now compare the incoming and previous editor state in a way that ignores key order. Specifically, we switched from `JSON.stringify()` to `dequal` for deep equality checks. ## Notes - This code only runs when external changes occur (e.g. after saving a document or when custom code modifies form fields). - Because this check is infrequent, performance impact is negligible. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211488746010087
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { EditorState, SerializedEditorState } from 'lexical'
|
||||
import type { Validate } from 'payload'
|
||||
|
||||
import {
|
||||
FieldDescription,
|
||||
@@ -12,6 +11,8 @@ import {
|
||||
useField,
|
||||
} from '@payloadcms/ui'
|
||||
import { mergeFieldStyles } from '@payloadcms/ui/shared'
|
||||
import { dequal } from 'dequal/lite'
|
||||
import { type Validate } from 'payload'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ErrorBoundary } from 'react-error-boundary'
|
||||
|
||||
@@ -140,10 +141,13 @@ const RichTextComponent: React.FC<
|
||||
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
|
||||
// the new value is the same as the old one is not necessary.
|
||||
// In postgres, the order of keys in JSON objects is not guaranteed to be preserved,
|
||||
// so we need to do a deep equality check here that does not care about key order => we use dequal.
|
||||
// If we used JSON.stringify, the editor would re-mount every time you save the document, as the order of keys changes => change detected => re-mount.
|
||||
if (
|
||||
prevValueRef.current !== value &&
|
||||
JSON.stringify(prevValueRef.current) !== JSON.stringify(value)
|
||||
!dequal(JSON.parse(JSON.stringify(prevValueRef.current)), value)
|
||||
) {
|
||||
prevInitialValueRef.current = initialValue
|
||||
prevValueRef.current = value
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Config } from '../../../payload-types.js'
|
||||
|
||||
import { ensureCompilationIsDone, saveDocAndAssert } from '../../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../../helpers/adminUrlUtil.js'
|
||||
import { assertNetworkRequests } from '../../../../helpers/e2e/assertNetworkRequests.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { reInitializeDB } from '../../../../helpers/reInitializeDB.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../../../../playwright.config.js'
|
||||
@@ -123,5 +124,53 @@ describe('Lexical Fully Featured - database', () => {
|
||||
// @ts-expect-error unsafe access is fine in tests
|
||||
expect(uploadNode.value?.filename).toBe('payload-1.jpg')
|
||||
})
|
||||
|
||||
test('ensure block contents are not reset on save on both create and update', async ({
|
||||
page,
|
||||
}) => {
|
||||
await lexical.slashCommand('myblock')
|
||||
await expect(lexical.editor.locator('.lexical-block')).toBeVisible()
|
||||
|
||||
/**
|
||||
* Test on create
|
||||
*/
|
||||
await assertNetworkRequests(
|
||||
page,
|
||||
`/admin/collections/${lexicalFullyFeaturedSlug}`,
|
||||
async () => {
|
||||
await lexical.editor.locator('#field-someText').first().fill('Testing 123')
|
||||
},
|
||||
{
|
||||
minimumNumberOfRequests: 2,
|
||||
allowedNumberOfRequests: 3,
|
||||
},
|
||||
)
|
||||
|
||||
await expect(lexical.editor.locator('#field-someText')).toHaveValue('Testing 123')
|
||||
await saveDocAndAssert(page)
|
||||
await expect(lexical.editor.locator('#field-someText')).toHaveValue('Testing 123')
|
||||
await page.reload()
|
||||
await expect(lexical.editor.locator('#field-someText')).toHaveValue('Testing 123')
|
||||
|
||||
/**
|
||||
* Test on update (this is where the issue appeared)
|
||||
*/
|
||||
await assertNetworkRequests(
|
||||
page,
|
||||
`/admin/collections/${lexicalFullyFeaturedSlug}`,
|
||||
async () => {
|
||||
await lexical.editor.locator('#field-someText').first().fill('Updated text')
|
||||
},
|
||||
{
|
||||
minimumNumberOfRequests: 2,
|
||||
allowedNumberOfRequests: 2,
|
||||
},
|
||||
)
|
||||
await expect(lexical.editor.locator('#field-someText')).toHaveValue('Updated text')
|
||||
await saveDocAndAssert(page)
|
||||
await expect(lexical.editor.locator('#field-someText')).toHaveValue('Updated text')
|
||||
await page.reload()
|
||||
await expect(lexical.editor.locator('#field-someText')).toHaveValue('Updated text')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user