= {
TextFields,
Uploads,
ArrayFields,
+ OnDemandForm,
+ OnDemandOutsideForm,
],
globals: [TabsWithRichText],
diff --git a/test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts b/test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts
deleted file mode 100644
index e7a2ae467..000000000
--- a/test/lexical/collections/LexicalLocalized/textToLexicalJSON.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import type { SerializedRelationshipNode } from '@payloadcms/richtext-lexical'
-import type {
- SerializedEditorState,
- SerializedParagraphNode,
- SerializedTextNode,
-} from '@payloadcms/richtext-lexical/lexical'
-
-import { lexicalLocalizedFieldsSlug } from '../../slugs.js'
-
-export function textToLexicalJSON({
- text,
- lexicalLocalizedRelID,
-}: {
- lexicalLocalizedRelID?: number | string
- text: string
-}): any {
- 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,
- textFormat: 0,
- type: 'paragraph',
- textStyle: '',
- version: 1,
- } as SerializedParagraphNode,
- ],
- },
- }
-
- if (lexicalLocalizedRelID) {
- editorJSON.root.children.push({
- format: '',
- type: 'relationship',
- version: 2,
- relationTo: lexicalLocalizedFieldsSlug,
- value: lexicalLocalizedRelID,
- } as SerializedRelationshipNode)
- }
-
- return editorJSON
-}
diff --git a/test/lexical/collections/OnDemandForm/Component.tsx b/test/lexical/collections/OnDemandForm/Component.tsx
new file mode 100644
index 000000000..9d4167169
--- /dev/null
+++ b/test/lexical/collections/OnDemandForm/Component.tsx
@@ -0,0 +1,20 @@
+'use client'
+
+import type { JSONFieldClientComponent } from 'payload'
+
+import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'
+
+import { lexicalFullyFeaturedSlug } from '../../slugs.js'
+
+export const Component: JSONFieldClientComponent = () => {
+ return (
+
+ Fully-Featured Component:
+
+
+ )
+}
diff --git a/test/lexical/collections/OnDemandForm/e2e.spec.ts b/test/lexical/collections/OnDemandForm/e2e.spec.ts
new file mode 100644
index 000000000..19c877133
--- /dev/null
+++ b/test/lexical/collections/OnDemandForm/e2e.spec.ts
@@ -0,0 +1,106 @@
+import { expect, test } from '@playwright/test'
+import { AdminUrlUtil } from 'helpers/adminUrlUtil.js'
+import { reInitializeDB } from 'helpers/reInitializeDB.js'
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+import { ensureCompilationIsDone, saveDocAndAssert } from '../../../helpers.js'
+import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
+import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
+import { LexicalHelpers } from '../utils.js'
+const filename = fileURLToPath(import.meta.url)
+const currentFolder = path.dirname(filename)
+const dirname = path.resolve(currentFolder, '../../')
+
+const { beforeAll, beforeEach, describe } = test
+
+const { serverURL } = await initPayloadE2ENoConfig({
+ dirname,
+})
+
+describe('Lexical On Demand', () => {
+ let lexical: LexicalHelpers
+ beforeAll(async ({ browser }, testInfo) => {
+ testInfo.setTimeout(TEST_TIMEOUT_LONG)
+ process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
+ const page = await browser.newPage()
+ await ensureCompilationIsDone({ page, serverURL })
+ await page.close()
+ })
+
+ describe('within form', () => {
+ beforeEach(async ({ page }) => {
+ await reInitializeDB({
+ serverURL,
+ snapshotKey: 'lexicalTest',
+ uploadsDir: [path.resolve(dirname, './collections/Upload/uploads')],
+ })
+ const url = new AdminUrlUtil(serverURL, 'OnDemandForm')
+ lexical = new LexicalHelpers(page)
+ await page.goto(url.create)
+ await lexical.editor.first().focus()
+ })
+ test('lexical is rendered on demand within form', async ({ page }) => {
+ await page.keyboard.type('Hello')
+
+ await saveDocAndAssert(page)
+ await page.reload()
+
+ const paragraph = lexical.editor.locator('> p')
+ await expect(paragraph).toHaveText('Hello')
+ })
+
+ test('on-demand editor within form can render nested fields', async () => {
+ await lexical.slashCommand('table', false)
+
+ await expect(lexical.drawer.locator('#field-rows')).toHaveValue('5')
+ await expect(lexical.drawer.locator('#field-columns')).toHaveValue('5')
+ })
+ })
+
+ describe('outside form', () => {
+ beforeEach(async ({ page }) => {
+ await reInitializeDB({
+ serverURL,
+ snapshotKey: 'lexicalTest',
+ uploadsDir: [path.resolve(dirname, './collections/Upload/uploads')],
+ })
+ const url = new AdminUrlUtil(serverURL, 'OnDemandOutsideForm')
+ lexical = new LexicalHelpers(page)
+ await page.goto(url.create)
+ await lexical.editor.first().focus()
+ })
+ test('lexical is rendered on demand outside form', async ({ page }) => {
+ await page.keyboard.type('Hello')
+
+ const paragraph = lexical.editor.locator('> p')
+ await expect(paragraph).toHaveText('Hellostate default')
+
+ await saveDocAndAssert(page)
+ await page.reload()
+
+ const paragraphAfterSave = lexical.editor.locator('> p')
+ await expect(paragraphAfterSave).not.toHaveText('Hellostate default') // Outside Form => Not Saved
+ })
+
+ test('lexical value can be controlled outside form', async ({ page }) => {
+ await page.keyboard.type('Hello')
+
+ const paragraph = lexical.editor.locator('> p')
+ await expect(paragraph).toHaveText('Hellostate default')
+
+ // Click button with text
+ const button = page.getByRole('button', { name: 'Reset Editor State' })
+ await button.click()
+
+ await expect(paragraph).toHaveText('state default')
+ })
+
+ test('on-demand editor outside form can render nested fields', async () => {
+ await lexical.slashCommand('table', false)
+
+ await expect(lexical.drawer.locator('#field-rows')).toHaveValue('5')
+ await expect(lexical.drawer.locator('#field-columns')).toHaveValue('5')
+ })
+ })
+})
diff --git a/test/lexical/collections/OnDemandForm/index.ts b/test/lexical/collections/OnDemandForm/index.ts
new file mode 100644
index 000000000..aef8a4240
--- /dev/null
+++ b/test/lexical/collections/OnDemandForm/index.ts
@@ -0,0 +1,16 @@
+import type { CollectionConfig } from 'payload'
+
+export const OnDemandForm: CollectionConfig = {
+ slug: 'OnDemandForm',
+ fields: [
+ {
+ name: 'json',
+ type: 'json',
+ admin: {
+ components: {
+ Field: './collections/OnDemandForm/Component.js#Component',
+ },
+ },
+ },
+ ],
+}
diff --git a/test/lexical/collections/OnDemandOutsideForm/Component.tsx b/test/lexical/collections/OnDemandOutsideForm/Component.tsx
new file mode 100644
index 000000000..d5a251661
--- /dev/null
+++ b/test/lexical/collections/OnDemandOutsideForm/Component.tsx
@@ -0,0 +1,35 @@
+'use client'
+
+import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
+import type { JSONFieldClientComponent } from 'payload'
+
+import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'
+import React, { useState } from 'react'
+
+import { lexicalFullyFeaturedSlug } from '../../slugs.js'
+
+export const Component: JSONFieldClientComponent = () => {
+ const [value, setValue] = useState(() =>
+ buildEditorState({ text: 'state default' }),
+ )
+
+ const handleReset = React.useCallback(() => {
+ setValue(buildEditorState({ text: 'state default' }))
+ }, [])
+
+ return (
+
+ Default Component:
+
+
+ Reset Editor State
+
+
+ )
+}
diff --git a/test/lexical/collections/OnDemandOutsideForm/index.ts b/test/lexical/collections/OnDemandOutsideForm/index.ts
new file mode 100644
index 000000000..d16c0d12b
--- /dev/null
+++ b/test/lexical/collections/OnDemandOutsideForm/index.ts
@@ -0,0 +1,16 @@
+import type { CollectionConfig } from 'payload'
+
+export const OnDemandOutsideForm: CollectionConfig = {
+ slug: 'OnDemandOutsideForm',
+ fields: [
+ {
+ name: 'json',
+ type: 'json',
+ admin: {
+ components: {
+ Field: './collections/OnDemandOutsideForm/Component.js#Component',
+ },
+ },
+ },
+ ],
+}
diff --git a/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts b/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts
index 946240713..19fdf13b9 100644
--- a/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts
+++ b/test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts
@@ -16,7 +16,7 @@ const dirname = path.resolve(currentFolder, '../../')
const { beforeAll, beforeEach, describe } = test
// Unlike the other suites, this one runs in parallel, as they run on the `lexical-fully-featured/create` URL and are "pure" tests
-test.describe.configure({ mode: 'parallel' })
+//test.describe.configure({ mode: 'parallel' })
const { serverURL } = await initPayloadE2ENoConfig({
dirname,
@@ -46,6 +46,7 @@ describe('Lexical Fully Featured', () => {
page,
}) => {
await lexical.slashCommand('block')
+ await expect(lexical.editor.locator('.lexical-block')).toBeVisible()
await lexical.slashCommand('relationship')
await lexical.drawer.locator('.list-drawer__header').getByText('Create New').click()
await lexical.save('drawer')
diff --git a/test/lexical/collections/utils.ts b/test/lexical/collections/utils.ts
index 867acafee..29d16f55e 100644
--- a/test/lexical/collections/utils.ts
+++ b/test/lexical/collections/utils.ts
@@ -1,6 +1,7 @@
import type { Locator, Page } from 'playwright'
import { expect } from '@playwright/test'
+import { wait } from 'payload/shared'
export class LexicalHelpers {
page: Page
@@ -98,16 +99,20 @@ export class LexicalHelpers {
async slashCommand(
// prettier-ignore
- command: 'block' | 'check' | 'code' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' |'h6' | 'inline'
- | 'link' | 'ordered' | 'paragraph' | 'quote' | 'relationship' | 'unordered' | 'upload',
+ command: ('block' | 'check' | 'code' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' |'h6' | 'inline'
+ | 'link' | 'ordered' | 'paragraph' | 'quote' | 'relationship' | 'table' | 'unordered'|'upload') | ({} & string),
+ expectMenuToClose = true,
) {
await this.page.keyboard.press(`/`)
const slashMenuPopover = this.page.locator('#slash-menu .slash-menu-popup')
await expect(slashMenuPopover).toBeVisible()
await this.page.keyboard.type(command)
+ await wait(200)
await this.page.keyboard.press(`Enter`)
- await expect(slashMenuPopover).toBeHidden()
+ if (expectMenuToClose) {
+ await expect(slashMenuPopover).toBeHidden()
+ }
}
get decorator() {
diff --git a/test/lexical/lexical.int.spec.ts b/test/lexical/lexical.int.spec.ts
index 1c06b3c94..ec8980272 100644
--- a/test/lexical/lexical.int.spec.ts
+++ b/test/lexical/lexical.int.spec.ts
@@ -1,16 +1,17 @@
-/* eslint-disable jest/no-conditional-in-test */
-import type {
- SerializedBlockNode,
- SerializedLinkNode,
- SerializedRelationshipNode,
- SerializedUploadNode,
-} from '@payloadcms/richtext-lexical'
import type {
SerializedEditorState,
SerializedParagraphNode,
} from '@payloadcms/richtext-lexical/lexical'
import type { PaginatedDocs, Payload } from 'payload'
+/* eslint-disable jest/no-conditional-in-test */
+import {
+ buildEditorState,
+ type SerializedBlockNode,
+ type SerializedLinkNode,
+ type SerializedRelationshipNode,
+ type SerializedUploadNode,
+} from '@payloadcms/richtext-lexical'
import path from 'path'
import { fileURLToPath } from 'url'
@@ -21,7 +22,6 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { NextRESTClient } from '../helpers/NextRESTClient.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'
@@ -655,7 +655,7 @@ describe('Lexical', () => {
locale: 'en',
data: {
title: 'Localized Lexical hooks',
- lexicalBlocksLocalized: textToLexicalJSON({ text: 'some text' }),
+ lexicalBlocksLocalized: buildEditorState({ text: 'some text' }),
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
'Shared text',
'English text in block',
diff --git a/test/lexical/payload-types.ts b/test/lexical/payload-types.ts
index 63f9ecc24..aba744f61 100644
--- a/test/lexical/payload-types.ts
+++ b/test/lexical/payload-types.ts
@@ -97,6 +97,8 @@ export interface Config {
'text-fields': TextField;
uploads: Upload;
'array-fields': ArrayField;
+ OnDemandForm: OnDemandForm;
+ OnDemandOutsideForm: OnDemandOutsideForm;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -118,6 +120,8 @@ export interface Config {
'text-fields': TextFieldsSelect | TextFieldsSelect;
uploads: UploadsSelect | UploadsSelect;
'array-fields': ArrayFieldsSelect | ArrayFieldsSelect;
+ OnDemandForm: OnDemandFormSelect | OnDemandFormSelect;
+ OnDemandOutsideForm: OnDemandOutsideFormSelect | OnDemandOutsideFormSelect;
users: UsersSelect | UsersSelect;
'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect;
'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect;
@@ -169,7 +173,7 @@ export interface LexicalFullyFeatured {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -193,7 +197,7 @@ export interface LexicalLinkFeature {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -217,7 +221,7 @@ export interface LexicalJsxConverter {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -242,7 +246,7 @@ export interface LexicalField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -257,7 +261,7 @@ export interface LexicalField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -272,7 +276,7 @@ export interface LexicalField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -298,7 +302,7 @@ export interface LexicalMigrateField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -313,7 +317,7 @@ export interface LexicalMigrateField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -328,7 +332,7 @@ export interface LexicalMigrateField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -345,7 +349,7 @@ export interface LexicalMigrateField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -364,7 +368,7 @@ export interface LexicalMigrateField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -396,7 +400,7 @@ export interface LexicalLocalizedField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -414,7 +418,7 @@ export interface LexicalLocalizedField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -438,7 +442,7 @@ export interface LexicalObjectReferenceBug {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -453,7 +457,7 @@ export interface LexicalObjectReferenceBug {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -477,7 +481,7 @@ export interface LexicalInBlock {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -494,7 +498,7 @@ export interface LexicalInBlock {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -524,7 +528,7 @@ export interface LexicalAccessControl {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -548,7 +552,7 @@ export interface LexicalRelationshipField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -563,7 +567,7 @@ export interface LexicalRelationshipField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -588,7 +592,7 @@ export interface RichTextField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -607,7 +611,7 @@ export interface RichTextField {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -828,6 +832,42 @@ export interface ArrayField {
updatedAt: string;
createdAt: string;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "OnDemandForm".
+ */
+export interface OnDemandForm {
+ id: string;
+ json?:
+ | {
+ [k: string]: unknown;
+ }
+ | unknown[]
+ | string
+ | number
+ | boolean
+ | null;
+ updatedAt: string;
+ createdAt: string;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "OnDemandOutsideForm".
+ */
+export interface OnDemandOutsideForm {
+ id: string;
+ json?:
+ | {
+ [k: string]: unknown;
+ }
+ | unknown[]
+ | string
+ | number
+ | boolean
+ | null;
+ updatedAt: string;
+ createdAt: string;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -915,6 +955,14 @@ export interface PayloadLockedDocument {
relationTo: 'array-fields';
value: string | ArrayField;
} | null)
+ | ({
+ relationTo: 'OnDemandForm';
+ value: string | OnDemandForm;
+ } | null)
+ | ({
+ relationTo: 'OnDemandOutsideForm';
+ value: string | OnDemandOutsideForm;
+ } | null)
| ({
relationTo: 'users';
value: string | User;
@@ -1286,6 +1334,24 @@ export interface ArrayFieldsSelect {
updatedAt?: T;
createdAt?: T;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "OnDemandForm_select".
+ */
+export interface OnDemandFormSelect {
+ json?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "OnDemandOutsideForm_select".
+ */
+export interface OnDemandOutsideFormSelect {
+ json?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
@@ -1351,7 +1417,7 @@ export interface TabsWithRichText {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
@@ -1368,7 +1434,7 @@ export interface TabsWithRichText {
root: {
type: string;
children: {
- type: string;
+ type: any;
version: number;
[k: string]: unknown;
}[];
diff --git a/test/lexical/seed.ts b/test/lexical/seed.ts
index 3e6d0f7a5..c249b4888 100644
--- a/test/lexical/seed.ts
+++ b/test/lexical/seed.ts
@@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url'
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 { richTextBulletsDocData, richTextDocData } from './collections/RichText/data.js'
import {
@@ -23,6 +22,7 @@ import {
// import type { Payload } from 'payload'
+import { buildEditorState } from '@payloadcms/richtext-lexical'
import { getFileByPath } from 'payload'
import { devUser } from '../credentials.js'
@@ -41,7 +41,6 @@ import { uploadsDoc } from './collections/Upload/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'
// import { pointDoc } from './collections/Point/shared.js'
@@ -215,7 +214,7 @@ export const seed = async (_payload: Payload) => {
collection: lexicalLocalizedFieldsSlug,
data: {
title: 'Localized Lexical en',
- lexicalBlocksLocalized: textToLexicalJSON({ text: 'English text' }),
+ lexicalBlocksLocalized: buildEditorState({ text: 'English text' }),
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
'Shared text',
'English text in block',
@@ -229,7 +228,7 @@ export const seed = async (_payload: Payload) => {
await _payload.create({
collection: lexicalRelationshipFieldsSlug,
data: {
- richText: textToLexicalJSON({ text: 'English text' }),
+ richText: buildEditorState({ text: 'English text' }),
},
depth: 0,
overrideAccess: true,
@@ -240,7 +239,7 @@ export const seed = async (_payload: Payload) => {
id: lexicalLocalizedDoc1.id,
data: {
title: 'Localized Lexical es',
- lexicalBlocksLocalized: textToLexicalJSON({ text: 'Spanish text' }),
+ lexicalBlocksLocalized: buildEditorState({ text: 'Spanish text' }),
lexicalBlocksSubLocalized: generateLexicalLocalizedRichText(
'Shared text',
'Spanish text in block',
@@ -257,13 +256,29 @@ export const seed = async (_payload: Payload) => {
data: {
title: 'Localized Lexical en 2',
- lexicalBlocksLocalized: textToLexicalJSON({
+ lexicalBlocksLocalized: buildEditorState({
text: 'English text 2',
- lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
+ nodes: [
+ {
+ format: '',
+ type: 'relationship',
+ version: 2,
+ relationTo: lexicalLocalizedFieldsSlug,
+ value: lexicalLocalizedDoc1.id,
+ },
+ ],
}),
- lexicalBlocksSubLocalized: textToLexicalJSON({
+ lexicalBlocksSubLocalized: buildEditorState({
text: 'English text 2',
- lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
+ nodes: [
+ {
+ format: '',
+ type: 'relationship',
+ version: 2,
+ relationTo: lexicalLocalizedFieldsSlug,
+ value: lexicalLocalizedDoc1.id,
+ },
+ ],
}),
},
locale: 'en',
@@ -277,9 +292,17 @@ export const seed = async (_payload: Payload) => {
data: {
title: 'Localized Lexical es 2',
- lexicalBlocksLocalized: textToLexicalJSON({
+ lexicalBlocksLocalized: buildEditorState({
text: 'Spanish text 2',
- lexicalLocalizedRelID: lexicalLocalizedDoc1.id,
+ nodes: [
+ {
+ format: '',
+ type: 'relationship',
+ version: 2,
+ relationTo: lexicalLocalizedFieldsSlug,
+ value: lexicalLocalizedDoc1.id,
+ },
+ ],
}),
},
locale: 'es',
@@ -317,7 +340,7 @@ export const seed = async (_payload: Payload) => {
version: 2,
fields: {
id: '6773773284be8978db7a498d',
- lexicalInBlock: textToLexicalJSON({ text: 'text' }),
+ lexicalInBlock: buildEditorState({ text: 'text' }),
blockName: '',
blockType: 'blockInLexical',
},
@@ -334,12 +357,12 @@ export const seed = async (_payload: Payload) => {
{
blockType: 'lexicalInBlock2',
blockName: '1',
- lexical: textToLexicalJSON({ text: '1' }),
+ lexical: buildEditorState({ text: '1' }),
},
{
blockType: 'lexicalInBlock2',
blockName: '2',
- lexical: textToLexicalJSON({ text: '2' }),
+ lexical: buildEditorState({ text: '2' }),
},
{
blockType: 'lexicalInBlock2',
diff --git a/test/package.json b/test/package.json
index ea399adf4..01ab67f5d 100644
--- a/test/package.json
+++ b/test/package.json
@@ -63,9 +63,9 @@
"@sentry/nextjs": "^8.33.1",
"@sentry/react": "^7.77.0",
"@types/jest": "29.5.12",
- "@types/react": "19.1.8",
- "@types/react-dom": "19.1.6",
- "babel-plugin-react-compiler": "19.1.0-rc.2",
+ "@types/react": "19.1.12",
+ "@types/react-dom": "19.1.9",
+ "babel-plugin-react-compiler": "19.1.0-rc.3",
"comment-json": "^4.2.3",
"create-payload-app": "workspace:*",
"csv-parse": "^5.6.0",
@@ -87,8 +87,8 @@
"payload": "workspace:*",
"pg": "8.16.3",
"qs-esm": "7.0.2",
- "react": "19.1.0",
- "react-dom": "19.1.0",
+ "react": "19.1.1",
+ "react-dom": "19.1.1",
"sass": "1.77.4",
"server-only": "^0.0.1",
"sharp": "0.32.6",
diff --git a/test/types/config.ts b/test/types/config.ts
index f33cf0816..27bb87579 100644
--- a/test/types/config.ts
+++ b/test/types/config.ts
@@ -18,6 +18,11 @@ export default buildConfigWithDefaults({
type: 'text',
name: 'text',
},
+ {
+ type: 'richText',
+ name: 'richText',
+ required: true,
+ },
{
type: 'text',
name: 'title',
diff --git a/test/types/payload-types.ts b/test/types/payload-types.ts
index 543350e28..59fcc859e 100644
--- a/test/types/payload-types.ts
+++ b/test/types/payload-types.ts
@@ -142,6 +142,21 @@ export interface UserAuthOperations {
export interface Post {
id: string;
text?: string | null;
+ richText: {
+ root: {
+ type: string;
+ children: {
+ type: any;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ };
title?: string | null;
selectField: MySelectOptions;
insideUnnamedGroup?: string | null;
@@ -193,6 +208,13 @@ export interface User {
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
+ sessions?:
+ | {
+ id: string;
+ createdAt?: string | null;
+ expiresAt: string;
+ }[]
+ | null;
password?: string | null;
}
/**
@@ -266,6 +288,7 @@ export interface PayloadMigration {
*/
export interface PostsSelect {
text?: T;
+ richText?: T;
title?: T;
selectField?: T;
insideUnnamedGroup?: T;
@@ -312,6 +335,13 @@ export interface UsersSelect {
hash?: T;
loginAttempts?: T;
lockUntil?: T;
+ sessions?:
+ | T
+ | {
+ id?: T;
+ createdAt?: T;
+ expiresAt?: T;
+ };
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -375,6 +405,6 @@ export interface Auth {
declare module 'payload' {
- // @ts-ignore
+ // @ts-ignore
export interface GeneratedTypes extends Config {}
-}
\ No newline at end of file
+}
diff --git a/test/types/types.spec.ts b/test/types/types.spec.ts
index 937823569..5829ecc28 100644
--- a/test/types/types.spec.ts
+++ b/test/types/types.spec.ts
@@ -1,3 +1,8 @@
+import type {
+ DefaultNodeTypes,
+ DefaultTypedEditorState,
+ TypedEditorState,
+} from '@payloadcms/richtext-lexical'
import type {
BulkOperationResult,
CustomDocumentViewConfig,
@@ -173,4 +178,107 @@ describe('Types testing', () => {
}>()
})
})
+
+ describe('lexical', () => {
+ type _Hardcoded_DefaultNodeTypes =
+ | 'autolink'
+ | 'heading'
+ | 'horizontalrule'
+ | 'linebreak'
+ | 'link'
+ | 'list'
+ | 'listitem'
+ | 'paragraph'
+ | 'quote'
+ | 'relationship'
+ | 'tab'
+ | 'text'
+ | 'upload'
+
+ test('ensure TypedEditorState node type without generic is string', () => {
+ expect().type.toBe()
+ })
+
+ test('ensure TypedEditorState<1 generic> node type is correct', () => {
+ expect<
+ TypedEditorState<{
+ type: 'custom-node'
+ version: 1
+ }>['root']['children'][number]['type']
+ >().type.toBe<'custom-node'>()
+ })
+
+ test('ensure TypedEditorState<2 generics> node type is correct', () => {
+ expect<
+ TypedEditorState<
+ | {
+ type: 'custom-node'
+ version: 1
+ }
+ | {
+ type: 'custom-node-2'
+ version: 1
+ }
+ >['root']['children'][number]['type']
+ >().type.toBe<'custom-node' | 'custom-node-2'>()
+ })
+
+ test('ensure DefaultTypedEditorState node type is a union of all possible node types', () => {
+ expect<
+ DefaultTypedEditorState['root']['children'][number]['type']
+ >().type.toBe<_Hardcoded_DefaultNodeTypes>()
+ })
+
+ test('ensure TypedEditorState node type is identical to DefaultTypedEditorState', () => {
+ expect<
+ TypedEditorState['root']['children'][number]['type']
+ >().type.toBe<_Hardcoded_DefaultNodeTypes>()
+ })
+
+ test('ensure DefaultTypedEditorState adds custom node type to union of default nodes', () => {
+ expect<
+ DefaultTypedEditorState<{
+ type: 'custom-node'
+ version: 1
+ }>['root']['children'][number]['type']
+ >().type.toBe<'custom-node' | _Hardcoded_DefaultNodeTypes>()
+ })
+
+ test('ensure DefaultTypedEditorState adds custom node types to union of default nodes', () => {
+ expect<
+ DefaultTypedEditorState<
+ | {
+ type: 'custom-node'
+ version: 1
+ }
+ | {
+ type: 'custom-node-2'
+ version: 1
+ }
+ >['root']['children'][number]['type']
+ >().type.toBe<'custom-node' | 'custom-node-2' | _Hardcoded_DefaultNodeTypes>()
+ })
+
+ test("ensure link node automatically narrows type so that node accepts fields property if type === 'link' is checked", () => {
+ type NodeType = DefaultTypedEditorState['root']['children'][number]
+
+ const node = {
+ type: 'link',
+ } as NodeType
+
+ if (node.type === 'link') {
+ expect(node).type.toHaveProperty('fields')
+ } else {
+ expect(node).type.not.toHaveProperty('fields')
+ }
+ })
+
+ test('ensure generated richText types can be assigned to DefaultTypedEditorState type', () => {
+ // If there is a function that expects DefaultTypedEditorState, you should be able to assign the generated type to it
+ // This ensures that data can be passed directly form the payload local API to a function that expects DefaultTypedEditorState
+ type GeneratedRichTextType = Post['richText']
+
+ expect().type.toBeAssignableWith()
+ })
+ })
})
diff --git a/test/versions/seed.ts b/test/versions/seed.ts
index df5d56841..0a47bcb9b 100644
--- a/test/versions/seed.ts
+++ b/test/versions/seed.ts
@@ -1,3 +1,4 @@
+import { buildEditorState } from '@payloadcms/richtext-lexical'
import path from 'path'
import { getFileByPath, type Payload } from 'payload'
import { fileURLToPath } from 'url'
@@ -14,7 +15,6 @@ import {
media2CollectionSlug,
mediaCollectionSlug,
} from './slugs.js'
-import { textToLexicalJSON } from './textToLexicalJSON.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -273,7 +273,7 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
textID: doc1ID,
updated: false,
}) as any,
- richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff' }),
+ richtextWithCustomDiff: buildEditorState({ text: 'richtextWithCustomDiff' }),
select: 'option1',
text: 'text',
textArea: 'textArea',
@@ -442,7 +442,7 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
textID: doc2ID,
updated: true,
}) as any,
- richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff2' }),
+ richtextWithCustomDiff: buildEditorState({ text: 'richtextWithCustomDiff2' }),
select: 'option2',
text: 'text2',
textArea: 'textArea2',
diff --git a/test/versions/textToLexicalJSON.ts b/test/versions/textToLexicalJSON.ts
deleted file mode 100644
index c2f12c9e5..000000000
--- a/test/versions/textToLexicalJSON.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type {
- SerializedEditorState,
- SerializedParagraphNode,
- SerializedTextNode,
-} from '@payloadcms/richtext-lexical/lexical'
-
-export function textToLexicalJSON({ text }: { text: string }): any {
- 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,
- textFormat: 0,
- type: 'paragraph',
- textStyle: '',
- version: 1,
- } as SerializedParagraphNode,
- ],
- },
- }
-
- return editorJSON
-}