Skip to content

Commit 1cc3bb9

Browse files
authored
fix: populate previousValue correctly in afterChange hooks for nested lexical fields (#15623)
## What Fixes `previousValue` being `undefined` in `afterChange` hook for nested Lexical fields. ## Why We had this issue previously where the wrong `previousValue` data was being accessed with nested fields, this introduced the fix of `previousValData` which properly scopes to sibling-level or full document data. When the previous fix was done, the `previousValue` in the rich text case here was missed. ## How Changed the `richText` case in `packages/payload/src/fields/hooks/afterChange/promise.ts` to use the same `previousValData` pattern as other field types: ```diff - previousValue: previousDoc?.[field.name], + previousValue: getNestedValue(previousValData, pathSegments) ?? previousValData?.[field.name], ``` ### Testing Added to `Lexical > lexical.int.spec.ts`
1 parent 4d4033b commit 1cc3bb9

File tree

5 files changed

+195
-3
lines changed

5 files changed

+195
-3
lines changed

packages/payload/src/fields/hooks/afterChange/promise.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ export const promise = async ({
289289
path: pathSegments,
290290
previousDoc,
291291
previousSiblingDoc,
292-
previousValue: previousDoc?.[field.name],
292+
previousValue:
293+
getNestedValue(previousValData, pathSegments) ?? previousValData?.[field.name],
293294
req,
294295
schemaPath: schemaPathSegments,
295296
siblingData,

test/lexical/baseConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
lexicalInlineBlocks,
1414
} from './collections/Lexical/index.js'
1515
import { LexicalAccessControl } from './collections/LexicalAccessControl/index.js'
16+
import { LexicalAutosave } from './collections/LexicalAutosave/index.js'
1617
import { LexicalHeadingFeature } from './collections/LexicalHeadingFeature/index.js'
1718
import { LexicalInBlock } from './collections/LexicalInBlock/index.js'
1819
import { LexicalJSXConverter } from './collections/LexicalJSXConverter/index.js'
@@ -37,6 +38,7 @@ export const baseConfig: Partial<Config> = {
3738
// ...extend config here
3839
collections: [
3940
LexicalFullyFeatured,
41+
LexicalAutosave,
4042
LexicalLinkFeature,
4143
LexicalListsFeature,
4244
LexicalHeadingFeature,
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { BlocksFeature, lexicalEditor, LinkFeature } from '@payloadcms/richtext-lexical'
4+
5+
import { lexicalAutosaveSlug } from '../../slugs.js'
6+
7+
export const autosaveHookLog: {
8+
relationshipField?: { operation: string; previousValue: unknown; value: unknown }
9+
} = {}
10+
11+
export const clearAutosaveHookLog = (): void => {
12+
delete autosaveHookLog.relationshipField
13+
}
14+
15+
export const LexicalAutosave: CollectionConfig = {
16+
slug: lexicalAutosaveSlug,
17+
versions: {
18+
drafts: {
19+
autosave: true,
20+
},
21+
},
22+
fields: [
23+
{
24+
name: 'title',
25+
type: 'text',
26+
},
27+
// The bug occurs when richText is nested inside an array (like a CTA array)
28+
{
29+
name: 'cta',
30+
type: 'array',
31+
fields: [
32+
{
33+
name: 'richText',
34+
type: 'richText',
35+
editor: lexicalEditor({
36+
features: ({ defaultFeatures }) => [
37+
...defaultFeatures,
38+
BlocksFeature({
39+
blocks: [
40+
{
41+
slug: 'textBlock',
42+
fields: [
43+
{
44+
name: 'blockTitle',
45+
type: 'text',
46+
hooks: {
47+
afterChange: [
48+
({ value, previousValue, operation }) => {
49+
autosaveHookLog.relationshipField = {
50+
operation,
51+
previousValue,
52+
value,
53+
}
54+
},
55+
],
56+
},
57+
},
58+
],
59+
},
60+
],
61+
}),
62+
LinkFeature({
63+
fields: [
64+
{
65+
type: 'blocks',
66+
name: 'linkBlocks',
67+
blocks: [
68+
{
69+
slug: 'linkBlock',
70+
fields: [
71+
{
72+
name: 'relationshipInLink',
73+
type: 'relationship',
74+
relationTo: 'text-fields',
75+
hooks: {
76+
afterChange: [
77+
({ value, previousValue, operation }) => {
78+
autosaveHookLog.relationshipField = {
79+
operation,
80+
previousValue,
81+
value,
82+
}
83+
},
84+
],
85+
},
86+
},
87+
],
88+
},
89+
],
90+
},
91+
],
92+
}),
93+
],
94+
}),
95+
},
96+
],
97+
},
98+
],
99+
}

test/lexical/lexical.int.spec.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import { beforeAll, beforeEach, describe, expect } from 'vitest'
1818

1919
import type { LexicalField, LexicalMigrateField, RichTextField } from './payload-types.js'
2020

21-
import { devUser } from '../credentials.js'
21+
import { it } from '../__helpers/int/vitest.js'
2222
import { initPayloadInt } from '../__helpers/shared/initPayloadInt.js'
2323
import { NextRESTClient } from '../__helpers/shared/NextRESTClient.js'
24-
import { it } from '../__helpers/int/vitest.js'
24+
import { devUser } from '../credentials.js'
2525
import { lexicalDocData } from './collections/Lexical/data.js'
2626
import { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js'
2727
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
@@ -836,4 +836,93 @@ describe('Lexical', () => {
836836
).rejects.toBeTruthy()
837837
})
838838
})
839+
840+
describe('Autosave', () => {
841+
it('should populate previousValue in afterChange hooks for fields inside lexical', async () => {
842+
const { autosaveHookLog, clearAutosaveHookLog } = await import(
843+
'./collections/LexicalAutosave/index.js'
844+
)
845+
846+
clearAutosaveHookLog()
847+
848+
const doc = await payload.create({
849+
collection: 'lexical-autosave',
850+
data: {
851+
title: 'Autosave test document',
852+
cta: [
853+
{
854+
richText: {
855+
root: {
856+
children: [
857+
{
858+
type: 'block',
859+
version: 2,
860+
format: '',
861+
fields: {
862+
id: 'block-id-1',
863+
blockName: '',
864+
blockTitle: 'Initial block title',
865+
blockType: 'textBlock',
866+
},
867+
},
868+
],
869+
direction: null,
870+
format: '',
871+
indent: 0,
872+
type: 'root',
873+
version: 1,
874+
},
875+
},
876+
},
877+
],
878+
},
879+
})
880+
881+
// Verify create operation has undefined previousValue (expected)
882+
expect(autosaveHookLog.relationshipField?.operation).toBe('create')
883+
expect(autosaveHookLog.relationshipField?.previousValue).toBeUndefined()
884+
expect(autosaveHookLog.relationshipField?.value).toBe('Initial block title')
885+
886+
clearAutosaveHookLog()
887+
888+
// Simulate autosave by updating the document
889+
await payload.update({
890+
collection: 'lexical-autosave',
891+
id: doc.id,
892+
data: {
893+
title: 'Updated via autosave',
894+
cta: [
895+
{
896+
richText: {
897+
root: {
898+
children: [
899+
{
900+
type: 'block',
901+
version: 2,
902+
format: '',
903+
fields: {
904+
id: 'block-id-1',
905+
blockName: '',
906+
blockTitle: 'Updated block title',
907+
blockType: 'textBlock',
908+
},
909+
},
910+
],
911+
direction: null,
912+
format: '',
913+
indent: 0,
914+
type: 'root',
915+
version: 1,
916+
},
917+
},
918+
},
919+
],
920+
},
921+
})
922+
923+
expect(autosaveHookLog.relationshipField?.operation).toBe('update')
924+
expect(autosaveHookLog.relationshipField?.previousValue).toBe('Initial block title')
925+
expect(autosaveHookLog.relationshipField?.value).toBe('Updated block title')
926+
})
927+
})
839928
})

test/lexical/slugs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
1111
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
1212
export const lexicalRelationshipFieldsSlug = 'lexical-relationship-fields'
1313
export const lexicalAccessControlSlug = 'lexical-access-control'
14+
export const lexicalAutosaveSlug = 'lexical-autosave'
1415
export const richTextFieldsSlug = 'rich-text-fields'
1516

1617
// Auxiliary slugs

0 commit comments

Comments
 (0)