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:
Alessio Gravili
2025-09-29 13:07:34 -07:00
committed by GitHub
parent 41aa201f7b
commit 6a2e8149f1
2 changed files with 56 additions and 3 deletions

View File

@@ -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')
})
})
})