feat(richtext-lexical)!: sub-field hooks and localization support (#6591)
## BREAKING - Our internal field hook methods now have new required `schemaPath` and path `props`. This affects the following functions, if you are using those: `afterChangeTraverseFields`, `afterReadTraverseFields`, `beforeChangeTraverseFields`, `beforeValidateTraverseFields`, `afterReadPromise` - The afterChange field hook's `value` is now the value AFTER the previous hooks were run. Previously, this was the original value, which I believe is a bug - Only relevant if you have built your own richText adapter: the richText adapter `populationPromises` property has been renamed to `graphQLPopulationPromises` and is now only run for graphQL. Previously, it was run for graphQL AND the rest API. To migrate, use `hooks.afterRead` to run population for the rest API - Only relevant if you have built your own lexical features: The `populationPromises` server feature property has been renamed to `graphQLPopulationPromises` and is now only run for graphQL. Previously, it was run for graphQL AND the rest API. To migrate, use `hooks.afterRead` to run population for the rest API - Serialized lexical link and upload nodes now have a new `id` property. While not breaking, localization / hooks will not work for their fields until you have migrated to that. Re-saving the old document on the new version will automatically add the `id` property for you. You will also get a bunch of console logs for every lexical node which is not migrated
This commit is contained in:
@@ -920,10 +920,9 @@ describe('lexicalBlocks', () => {
|
||||
await wait(300)
|
||||
|
||||
await page.click('#action-save', { delay: 100 })
|
||||
await wait(300)
|
||||
|
||||
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||
'The following field is invalid',
|
||||
'The following fields are invalid',
|
||||
)
|
||||
await wait(300)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export function generateLexicalRichText() {
|
||||
format: '',
|
||||
type: 'upload',
|
||||
version: 2,
|
||||
id: '665d105a91e1c337ba8308dd',
|
||||
fields: {
|
||||
caption: {
|
||||
root: {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
export function generateLexicalLocalizedRichText(text1: string, text2: string, blockID?: string) {
|
||||
return {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: text1,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
},
|
||||
{
|
||||
format: '',
|
||||
type: 'block',
|
||||
version: 2,
|
||||
fields: {
|
||||
id: blockID ?? '66685716795f191f08367b1a',
|
||||
blockName: '',
|
||||
textLocalized: text2,
|
||||
counter: 1,
|
||||
blockType: 'block',
|
||||
},
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,50 @@ export const LexicalLocalizedFields: CollectionConfig = {
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'lexicalSimple',
|
||||
name: 'lexicalBlocksSubLocalized',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
admin: {
|
||||
description: 'Non-localized field with localized block subfields',
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
fields: [
|
||||
{
|
||||
name: 'textLocalized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'counter',
|
||||
type: 'number',
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ value }) => {
|
||||
return value ? value + 1 : 1
|
||||
},
|
||||
],
|
||||
afterRead: [
|
||||
({ value }) => {
|
||||
return value ? value * 10 : 10
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rel',
|
||||
type: 'relationship',
|
||||
relationTo: lexicalLocalizedFieldsSlug,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -60,36 +99,5 @@ export const LexicalLocalizedFields: CollectionConfig = {
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'lexicalBlocksSubLocalized',
|
||||
type: 'richText',
|
||||
admin: {
|
||||
description: 'Non-localized field with localized block subfields',
|
||||
},
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
fields: [
|
||||
{
|
||||
name: 'textLocalized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'rel',
|
||||
type: 'relationship',
|
||||
relationTo: lexicalLocalizedFieldsSlug,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export function generateLexicalRichText() {
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
id: '665d10938106ab380c7f3730',
|
||||
type: 'link',
|
||||
version: 2,
|
||||
fields: {
|
||||
@@ -86,6 +87,7 @@ export function generateLexicalRichText() {
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
id: '665d10938106ab380c7f3730',
|
||||
type: 'link',
|
||||
version: 2,
|
||||
fields: {
|
||||
@@ -230,6 +232,7 @@ export function generateLexicalRichText() {
|
||||
format: '',
|
||||
type: 'upload',
|
||||
version: 2,
|
||||
id: '665d10938106ab380c7f372f',
|
||||
relationTo: 'uploads',
|
||||
value: '{{UPLOAD_DOC_ID}}',
|
||||
fields: {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { devUser } from '../credentials.js'
|
||||
import { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { lexicalDocData } from './collections/Lexical/data.js'
|
||||
import { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js'
|
||||
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
|
||||
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
|
||||
import { richTextDocData } from './collections/RichText/data.js'
|
||||
import { generateLexicalRichText } from './collections/RichText/generateLexicalRichText.js'
|
||||
@@ -569,4 +571,100 @@ describe('Lexical', () => {
|
||||
expect(populatedDocEditorRelationshipNode.value.text).toStrictEqual(textDoc.text)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Localization', () => {
|
||||
it('ensure localized lexical field is different across locales', async () => {
|
||||
const lexicalDocEN = await payload.find({
|
||||
collection: 'lexical-localized-fields',
|
||||
locale: 'en',
|
||||
where: {
|
||||
title: {
|
||||
equals: 'Localized Lexical en',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(lexicalDocEN.docs[0].lexicalBlocksLocalized.root.children[0].children[0].text).toEqual(
|
||||
'English text',
|
||||
)
|
||||
|
||||
const lexicalDocES = await payload.findByID({
|
||||
collection: 'lexical-localized-fields',
|
||||
locale: 'es',
|
||||
id: lexicalDocEN.docs[0].id,
|
||||
})
|
||||
|
||||
expect(lexicalDocES.lexicalBlocksLocalized.root.children[0].children[0].text).toEqual(
|
||||
'Spanish text',
|
||||
)
|
||||
})
|
||||
|
||||
it('ensure localized text field within blocks field within unlocalized lexical field is different across locales', async () => {
|
||||
const lexicalDocEN = await payload.find({
|
||||
collection: 'lexical-localized-fields',
|
||||
locale: 'en',
|
||||
where: {
|
||||
title: {
|
||||
equals: 'Localized Lexical en',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
lexicalDocEN.docs[0].lexicalBlocksSubLocalized.root.children[0].children[0].text,
|
||||
).toEqual('Shared text')
|
||||
|
||||
expect(
|
||||
(lexicalDocEN.docs[0].lexicalBlocksSubLocalized.root.children[1].fields as any)
|
||||
.textLocalized,
|
||||
).toEqual('English text in block')
|
||||
|
||||
const lexicalDocES = await payload.findByID({
|
||||
collection: 'lexical-localized-fields',
|
||||
locale: 'es',
|
||||
id: lexicalDocEN.docs[0].id,
|
||||
})
|
||||
|
||||
expect(lexicalDocES.lexicalBlocksSubLocalized.root.children[0].children[0].text).toEqual(
|
||||
'Shared text',
|
||||
)
|
||||
|
||||
expect(
|
||||
(lexicalDocES.lexicalBlocksSubLocalized.root.children[1].fields as any).textLocalized,
|
||||
).toEqual('Spanish text in block')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hooks', () => {
|
||||
it('ensure hook within number field within lexical block runs', async () => {
|
||||
const lexicalDocEN = await payload.create({
|
||||
collection: 'lexical-localized-fields',
|
||||
locale: 'en',
|
||||
data: {
|
||||
title: 'Localized Lexical hooks',
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'some text' }) as any,
|
||||
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||
'Shared text',
|
||||
'English text in block',
|
||||
) as any,
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
(lexicalDocEN.lexicalBlocksSubLocalized.root.children[1].fields as any).counter,
|
||||
).toEqual(20) // Initial: 1. BeforeChange: +1 (2). AfterRead: *10 (20)
|
||||
|
||||
// update document with same data
|
||||
const lexicalDocENUpdated = await payload.update({
|
||||
collection: 'lexical-localized-fields',
|
||||
locale: 'en',
|
||||
id: lexicalDocEN.id,
|
||||
data: lexicalDocEN,
|
||||
})
|
||||
|
||||
expect(
|
||||
(lexicalDocENUpdated.lexicalBlocksSubLocalized.root.children[1].fields as any).counter,
|
||||
).toEqual(210) // Initial: 20. BeforeChange: +1 (21). AfterRead: *10 (210)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -207,7 +207,7 @@ export interface LexicalMigrateField {
|
||||
export interface LexicalLocalizedField {
|
||||
id: string;
|
||||
title: string;
|
||||
lexicalSimple?: {
|
||||
lexicalBlocksSubLocalized?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
@@ -237,21 +237,6 @@ export interface LexicalLocalizedField {
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
lexicalBlocksSubLocalized?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { dateDoc } from './collections/Date/shared.js'
|
||||
import { groupDoc } from './collections/Group/shared.js'
|
||||
import { jsonDoc } from './collections/JSON/shared.js'
|
||||
import { lexicalDocData } from './collections/Lexical/data.js'
|
||||
import { generateLexicalLocalizedRichText } from './collections/LexicalLocalized/generateLexicalRichText.js'
|
||||
import { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
|
||||
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
|
||||
import { numberDoc } from './collections/Number/shared.js'
|
||||
@@ -281,9 +282,11 @@ export const seed = async (_payload: Payload) => {
|
||||
collection: lexicalLocalizedFieldsSlug,
|
||||
data: {
|
||||
title: 'Localized Lexical en',
|
||||
lexicalSimple: textToLexicalJSON({ text: 'English text' }),
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }),
|
||||
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'English text' }),
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }) as any,
|
||||
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||
'Shared text',
|
||||
'English text in block',
|
||||
) as any,
|
||||
},
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
@@ -295,9 +298,12 @@ export const seed = async (_payload: Payload) => {
|
||||
id: lexicalLocalizedDoc1.id,
|
||||
data: {
|
||||
title: 'Localized Lexical es',
|
||||
lexicalSimple: textToLexicalJSON({ text: 'Spanish text' }),
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }),
|
||||
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'Spanish text' }),
|
||||
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }) as any,
|
||||
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
|
||||
'Shared text',
|
||||
'Spanish text in block',
|
||||
(lexicalLocalizedDoc1.lexicalBlocksSubLocalized.root.children[1].fields as any).id,
|
||||
) as any,
|
||||
},
|
||||
locale: 'es',
|
||||
depth: 0,
|
||||
|
||||
Reference in New Issue
Block a user