Skip to content

Commit bb1501e

Browse files
fix: previousValue from hooks should be populated within lexical blocks (#14856)
### What? Updates the `previousValue` data sent to the `afterChange` hook to accomodate **lexical nested** paths. ### Why? Currently the `afterChange` hook uses the previousDoc: ```ts previousValue: getNestedValue(previousDoc, pathSegments) ?? previousDoc?.[field.name], ``` However, with fields from within Lexical, the data on the full `previousDoc` has a complex nesting structure. In this case, we should be using the `previousSiblingDoc` to only return the sibling data and easily de-structure the data. ### How? Checks if sibling data exists, and uses it if it does: ```ts const previousValData = previousSiblingDoc && Object.keys(previousSiblingDoc).length > 0 ? previousSiblingDoc : previousDoc ``` **Reported by client.** Related PR: #14582 --------- Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
1 parent be0e9b5 commit bb1501e

File tree

4 files changed

+192
-3
lines changed

4 files changed

+192
-3
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export const promise = async ({
7272
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
7373
const getNestedValue = (data: JsonObject, path: string[]) =>
7474
path.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), data)
75+
const previousValData =
76+
previousSiblingDoc && Object.keys(previousSiblingDoc).length > 0
77+
? previousSiblingDoc
78+
: previousDoc
7579

7680
if (fieldAffectsData(field)) {
7781
// Execute hooks
@@ -90,7 +94,8 @@ export const promise = async ({
9094
path: pathSegments,
9195
previousDoc,
9296
previousSiblingDoc,
93-
previousValue: getNestedValue(previousDoc, pathSegments) ?? previousDoc?.[field.name],
97+
previousValue:
98+
getNestedValue(previousValData, pathSegments) ?? previousValData?.[field.name],
9499
req,
95100
schemaPath: schemaPathSegments,
96101
siblingData,
@@ -172,7 +177,7 @@ export const promise = async ({
172177
parentPath: path + '.' + rowIndex,
173178
parentSchemaPath: schemaPath + '.' + block.slug,
174179
previousDoc,
175-
previousSiblingDoc: previousDoc?.[field.name]?.[rowIndex] || ({} as JsonObject),
180+
previousSiblingDoc: previousValData?.[field.name]?.[rowIndex] || ({} as JsonObject),
176181
req,
177182
siblingData: siblingData?.[field.name]?.[rowIndex] || {},
178183
siblingDoc: row ? { ...row } : {},

test/hooks/collections/NestedAfterChangeHook/index.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { CollectionConfig } from 'payload'
2+
3+
import { BlocksFeature, lexicalEditor, LinkFeature } from '@payloadcms/richtext-lexical'
24
export const nestedAfterChangeHooksSlug = 'nested-after-change-hooks'
35

46
const NestedAfterChangeHooks: CollectionConfig = {
@@ -22,7 +24,6 @@ const NestedAfterChangeHooks: CollectionConfig = {
2224
hooks: {
2325
afterChange: [
2426
({ previousValue, operation }) => {
25-
console.log(previousValue)
2627
if (operation === 'update' && typeof previousValue === 'undefined') {
2728
throw new Error('previousValue is missing in nested beforeChange hook')
2829
}
@@ -34,6 +35,68 @@ const NestedAfterChangeHooks: CollectionConfig = {
3435
},
3536
],
3637
},
38+
{
39+
name: 'lexical',
40+
type: 'richText',
41+
editor: lexicalEditor({
42+
features: ({ defaultFeatures }) => [
43+
...defaultFeatures,
44+
BlocksFeature({
45+
blocks: [
46+
{
47+
slug: 'nestedBlock',
48+
fields: [
49+
{
50+
type: 'text',
51+
name: 'nestedAfterChange',
52+
hooks: {
53+
afterChange: [
54+
({ previousValue, operation }) => {
55+
if (operation === 'update' && typeof previousValue === 'undefined') {
56+
throw new Error('previousValue is missing in nested beforeChange hook')
57+
}
58+
},
59+
],
60+
},
61+
},
62+
],
63+
},
64+
],
65+
}),
66+
LinkFeature({
67+
fields: [
68+
{
69+
type: 'blocks',
70+
name: 'linkBlocks',
71+
blocks: [
72+
{
73+
slug: 'nestedLinkBlock',
74+
fields: [
75+
{
76+
name: 'nestedRelationship',
77+
type: 'relationship',
78+
relationTo: 'relations',
79+
hooks: {
80+
afterChange: [
81+
({ previousValue, operation }) => {
82+
if (operation === 'update' && typeof previousValue === 'undefined') {
83+
throw new Error(
84+
'previousValue is missing in nested beforeChange hook',
85+
)
86+
}
87+
},
88+
],
89+
},
90+
},
91+
],
92+
},
93+
],
94+
},
95+
],
96+
}),
97+
],
98+
}),
99+
},
37100
],
38101
}
39102

test/hooks/int.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,111 @@ describe('Hooks', () => {
368368

369369
expect(updatedDoc).toBeDefined()
370370
})
371+
372+
it('should populate previousValue in Lexical nested afterChange hooks', async () => {
373+
const relationID = await payload.create({
374+
collection: 'relations',
375+
data: {
376+
title: 'Relation for nested afterChange',
377+
},
378+
})
379+
380+
// this collection will throw an error if previousValue is not defined in nested afterChange hook
381+
const nestedAfterChangeDoc = await payload.create({
382+
collection: nestedAfterChangeHooksSlug,
383+
data: {
384+
text: 'initial',
385+
group: {
386+
array: [
387+
{
388+
nestedAfterChange: 'initial',
389+
},
390+
],
391+
},
392+
lexical: {
393+
root: {
394+
children: [
395+
{
396+
children: [
397+
{
398+
children: [
399+
{
400+
detail: 0,
401+
format: 0,
402+
mode: 'normal',
403+
style: '',
404+
text: 'link',
405+
type: 'text',
406+
version: 1,
407+
},
408+
],
409+
direction: null,
410+
format: '',
411+
indent: 0,
412+
type: 'link',
413+
version: 3,
414+
fields: {
415+
linkBlocks: [
416+
{
417+
id: '693ade72068ea07ba13edcab',
418+
blockType: 'nestedLinkBlock',
419+
nestedRelationship: relationID.id,
420+
},
421+
],
422+
},
423+
id: '693ade70068ea07ba13edca9',
424+
},
425+
],
426+
direction: null,
427+
format: '',
428+
indent: 0,
429+
type: 'paragraph',
430+
version: 1,
431+
textFormat: 0,
432+
textStyle: '',
433+
},
434+
{
435+
type: 'block',
436+
version: 2,
437+
format: '',
438+
fields: {
439+
id: '693adf3c068ea07ba13edcae',
440+
blockName: '',
441+
nestedAfterChange: 'test',
442+
blockType: 'nestedBlock',
443+
},
444+
},
445+
{
446+
children: [],
447+
direction: null,
448+
format: '',
449+
indent: 0,
450+
type: 'paragraph',
451+
version: 1,
452+
textFormat: 0,
453+
textStyle: '',
454+
},
455+
],
456+
direction: null,
457+
format: '',
458+
indent: 0,
459+
type: 'root',
460+
version: 1,
461+
},
462+
},
463+
},
464+
})
465+
466+
await expect(
467+
payload.update({
468+
collection: 'nested-after-change-hooks',
469+
id: nestedAfterChangeDoc.id,
470+
data: {
471+
text: 'updated',
472+
},
473+
}),
474+
).resolves.not.toThrow()
475+
})
371476
})
372477

373478
describe('auth collection hooks', () => {

test/hooks/payload-types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,21 @@ export interface NestedAfterChangeHook {
288288
}[]
289289
| null;
290290
};
291+
lexical?: {
292+
root: {
293+
type: string;
294+
children: {
295+
type: any;
296+
version: number;
297+
[k: string]: unknown;
298+
}[];
299+
direction: ('ltr' | 'rtl') | null;
300+
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
301+
indent: number;
302+
version: number;
303+
};
304+
[k: string]: unknown;
305+
} | null;
291306
updatedAt: string;
292307
createdAt: string;
293308
}
@@ -943,6 +958,7 @@ export interface NestedAfterChangeHooksSelect<T extends boolean = true> {
943958
id?: T;
944959
};
945960
};
961+
lexical?: T;
946962
updatedAt?: T;
947963
createdAt?: T;
948964
}

0 commit comments

Comments
 (0)