feat(richtext-lexical)!: rework how population works and saves data, improve node typing

This commit is contained in:
Alessio Gravili
2024-04-17 11:46:47 -04:00
parent 58ea94f6ac
commit 39ba39c237
52 changed files with 709 additions and 420 deletions

View File

@@ -33,9 +33,15 @@ let serverURL: string
/**
* Client-side navigation to the lexical editor from list view
*/
async function navigateToLexicalFields(navigateToListView: boolean = true) {
async function navigateToLexicalFields(
navigateToListView: boolean = true,
localized: boolean = false,
) {
if (navigateToListView) {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'lexical-fields')
const url: AdminUrlUtil = new AdminUrlUtil(
serverURL,
localized ? 'lexical-localized-fields' : 'lexical-fields',
)
await page.goto(url.list)
}
@@ -1101,7 +1107,7 @@ describe('lexical', () => {
)
})
test.skip('should respect required error state in deeply nested text field', async () => {
test('should respect required error state in deeply nested text field', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
@@ -1117,12 +1123,18 @@ describe('lexical', () => {
await page.click('#action-save', { delay: 100 })
await expect(page.locator('.Toastify')).toContainText('The following field is invalid')
const requiredTooltip = conditionalArrayBlock
.locator('.tooltip-content:has-text("This field is required.")')
.first()
await requiredTooltip.scrollIntoViewIfNeeded()
// Check if error is shown next to field
await expect(
conditionalArrayBlock
.locator('.tooltip-content:has-text("This field is required.")')
.first(),
).toBeVisible()
await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
})
})
describe('localization', () => {
test.skip('ensure simple localized lexical field works', async () => {
await navigateToLexicalFields(true, true)
})
})
})

View File

@@ -27,7 +27,7 @@ export function generateLexicalRichText() {
{
format: '',
type: 'upload',
version: 1,
version: 2,
fields: {
caption: {
root: {
@@ -57,11 +57,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
version: 2,
relationTo: 'text-fields',
value: {
id: '{{TEXT_DOC_ID}}',
},
value: '{{TEXT_DOC_ID}}',
},
],
direction: 'ltr',
@@ -69,9 +67,7 @@ export function generateLexicalRichText() {
},
},
relationTo: 'uploads',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
value: '{{UPLOAD_DOC_ID}}',
},
{
format: '',
@@ -120,11 +116,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
version: 2,
relationTo: 'rich-text-fields',
value: {
id: '{{RICH_TEXT_DOC_ID}}',
},
value: '{{RICH_TEXT_DOC_ID}}',
},
{
children: [
@@ -173,11 +167,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
version: 2,
relationTo: 'text-fields',
value: {
id: '{{TEXT_DOC_ID}}',
},
value: '{{TEXT_DOC_ID}}',
},
{
children: [

View File

@@ -0,0 +1,95 @@
import type { CollectionConfig } from 'payload/types'
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
export const LexicalLocalizedFields: CollectionConfig = {
slug: lexicalLocalizedFieldsSlug,
admin: {
useAsTitle: 'title',
listSearchableFields: ['title'],
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
name: 'lexicalSimple',
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
},
{
name: 'lexicalBlocksLocalized',
admin: {
description: 'Localized field with localized block subfields',
},
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'block',
fields: [
{
name: 'textLocalized',
type: 'text',
localized: true,
},
{
name: 'rel',
type: 'relationship',
relationTo: lexicalLocalizedFieldsSlug,
},
],
},
],
}),
],
}),
},
{
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,
},
],
},
],
}),
],
}),
},
],
}

View File

@@ -0,0 +1,54 @@
import type { SerializedRelationshipNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical'
import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
export function textToLexicalJSON({
text,
lexicalLocalizedRelID,
}: {
lexicalLocalizedRelID?: number | string
text: string
}) {
const editorJSON: SerializedEditorState = {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
direction: 'ltr',
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
type: 'text',
version: 1,
} as SerializedTextNode,
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
} as SerializedParagraphNode,
],
},
}
if (lexicalLocalizedRelID) {
editorJSON.root.children.push({
format: '',
type: 'relationship',
version: 2,
relationTo: lexicalLocalizedFieldsSlug,
value: lexicalLocalizedRelID,
} as SerializedRelationshipNode)
}
return editorJSON
}

View File

@@ -116,10 +116,8 @@ export function generateLexicalRichText() {
{
format: '',
type: 'relationship',
version: 1,
value: {
id: '{{TEXT_DOC_ID}}',
},
version: 2,
value: '{{TEXT_DOC_ID}}',
relationTo: 'text-fields',
},
{
@@ -230,11 +228,9 @@ export function generateLexicalRichText() {
{
format: '',
type: 'upload',
version: 1,
version: 2,
relationTo: 'uploads',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
value: '{{UPLOAD_DOC_ID}}',
fields: {
caption: {
root: {

View File

@@ -12,6 +12,7 @@ import GroupFields from './collections/Group/index.js'
import IndexedFields from './collections/Indexed/index.js'
import JSONFields from './collections/JSON/index.js'
import { LexicalFields } from './collections/Lexical/index.js'
import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
import NumberFields from './collections/Number/index.js'
import PointFields from './collections/Point/index.js'
@@ -31,6 +32,7 @@ import { clearAndSeedEverything } from './seed.js'
export const collectionSlugs: CollectionConfig[] = [
LexicalFields,
LexicalMigrateFields,
LexicalLocalizedFields,
{
slug: 'users',
admin: {

View File

@@ -416,9 +416,10 @@ describe('Lexical', () => {
/**
* Depth 1 population:
*/
expect(subEditorRelationshipNode.value.id).toStrictEqual(createdRichTextDocID)
expect(subEditorRelationshipNode.value).toStrictEqual(createdRichTextDocID)
// But the value should not be populated and only have the id field:
expect(Object.keys(subEditorRelationshipNode.value)).toHaveLength(1)
expect(typeof subEditorRelationshipNode.value).not.toStrictEqual('object')
})
it('should populate relationship nodes inside of a sub-editor from a blocks node with 1 depth', async () => {
@@ -463,9 +464,9 @@ describe('Lexical', () => {
/**
* Depth 2 population:
*/
expect(populatedDocEditorRelationshipNode.value.id).toStrictEqual(createdTextDocID)
expect(populatedDocEditorRelationshipNode.value).toStrictEqual(createdTextDocID)
// But the value should not be populated and only have the id field - that's because it would require a depth of 2
expect(Object.keys(populatedDocEditorRelationshipNode.value)).toHaveLength(1)
expect(populatedDocEditorRelationshipNode.value).not.toStrictEqual('object')
})
it('should populate relationship nodes inside of a sub-editor from a blocks node with depth 2', async () => {

View File

@@ -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 { textToLexicalJSON } from './collections/LexicalLocalized/textToLexicalJSON.js'
import { lexicalMigrateDocData } from './collections/LexicalMigrate/data.js'
import { numberDoc } from './collections/Number/shared.js'
import { pointDoc } from './collections/Point/shared.js'
@@ -35,6 +36,7 @@ import {
groupFieldsSlug,
jsonFieldsSlug,
lexicalFieldsSlug,
lexicalLocalizedFieldsSlug,
lexicalMigrateFieldsSlug,
numberFieldsSlug,
pointFieldsSlug,
@@ -49,7 +51,7 @@ import {
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export const seed = async (_payload) => {
export const seed = async (_payload: Payload) => {
if (_payload.db.name === 'mongoose') {
await Promise.all(
_payload.config.collections.map(async (coll) => {
@@ -274,6 +276,74 @@ export const seed = async (_payload) => {
overrideAccess: true,
})
const lexicalLocalizedDoc1 = await _payload.create({
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en',
lexicalSimple: textToLexicalJSON({ text: 'English text' }),
lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }),
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'English text' }),
},
locale: 'en',
depth: 0,
overrideAccess: true,
})
await _payload.update({
collection: lexicalLocalizedFieldsSlug,
id: lexicalLocalizedDoc1.id,
data: {
title: 'Localized Lexical es',
lexicalSimple: textToLexicalJSON({ text: 'Spanish text' }),
lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }),
lexicalBlocksSubLocalized: textToLexicalJSON({ text: 'Spanish text' }),
},
locale: 'es',
depth: 0,
overrideAccess: true,
})
const lexicalLocalizedDoc2 = await _payload.create({
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en 2',
lexicalSimple: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksLocalized: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksSubLocalized: textToLexicalJSON({
text: 'English text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
},
locale: 'en',
depth: 0,
overrideAccess: true,
})
await _payload.update({
collection: lexicalLocalizedFieldsSlug,
id: lexicalLocalizedDoc2.id,
data: {
title: 'Localized Lexical es 2',
lexicalSimple: textToLexicalJSON({
text: 'Spanish text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
lexicalBlocksLocalized: textToLexicalJSON({
text: 'Spanish text 2',
lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
}),
},
locale: 'es',
depth: 0,
overrideAccess: true,
})
await _payload.create({
collection: lexicalMigrateFieldsSlug,
data: lexicalMigrateDocWithRelId,

View File

@@ -1,28 +1,29 @@
export const usersSlug = 'users' as const
export const arrayFieldsSlug = 'array-fields' as const
export const blockFieldsSlug = 'block-fields' as const
export const checkboxFieldsSlug = 'checkbox-fields' as const
export const codeFieldsSlug = 'code-fields' as const
export const collapsibleFieldsSlug = 'collapsible-fields' as const
export const conditionalLogicSlug = 'conditional-logic' as const
export const dateFieldsSlug = 'date-fields' as const
export const groupFieldsSlug = 'group-fields' as const
export const indexedFieldsSlug = 'indexed-fields' as const
export const jsonFieldsSlug = 'json-fields' as const
export const lexicalFieldsSlug = 'lexical-fields' as const
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' as const
export const numberFieldsSlug = 'number-fields' as const
export const pointFieldsSlug = 'point-fields' as const
export const radioFieldsSlug = 'radio-fields' as const
export const relationshipFieldsSlug = 'relationship-fields' as const
export const richTextFieldsSlug = 'rich-text-fields' as const
export const rowFieldsSlug = 'row-fields' as const
export const selectFieldsSlug = 'select-fields' as const
export const tabsFieldsSlug = 'tabs-fields' as const
export const textFieldsSlug = 'text-fields' as const
export const uploadsSlug = 'uploads' as const
export const uploads2Slug = 'uploads2' as const
export const uploads3Slug = 'uploads3' as const
export const usersSlug = 'users'
export const arrayFieldsSlug = 'array-fields'
export const blockFieldsSlug = 'block-fields'
export const checkboxFieldsSlug = 'checkbox-fields'
export const codeFieldsSlug = 'code-fields'
export const collapsibleFieldsSlug = 'collapsible-fields'
export const conditionalLogicSlug = 'conditional-logic'
export const dateFieldsSlug = 'date-fields'
export const groupFieldsSlug = 'group-fields'
export const indexedFieldsSlug = 'indexed-fields'
export const jsonFieldsSlug = 'json-fields'
export const lexicalFieldsSlug = 'lexical-fields'
export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
export const numberFieldsSlug = 'number-fields'
export const pointFieldsSlug = 'point-fields'
export const radioFieldsSlug = 'radio-fields'
export const relationshipFieldsSlug = 'relationship-fields'
export const richTextFieldsSlug = 'rich-text-fields'
export const rowFieldsSlug = 'row-fields'
export const selectFieldsSlug = 'select-fields'
export const tabsFieldsSlug = 'tabs-fields'
export const textFieldsSlug = 'text-fields'
export const uploadsSlug = 'uploads'
export const uploads2Slug = 'uploads2'
export const uploads3Slug = 'uploads3'
export const collectionSlugs = [
usersSlug,
arrayFieldsSlug,