chore: separate Lexical tests into dedicated suite (#12047)

Lexical tests comprise almost half of the collections in the fields
suite, and are starting to become complex to manage.

They are sometimes related to other auxiliary collections, so
refactoring one test sometimes breaks another, seemingly unrelated one.

In addition, the fields suite is very large, taking a long time to
compile. This will make it faster.

Some ideas for future refactorings:
- 3 main collections: defaultFeatures, fully featured, and legacy.
Legacy is the current one that has multiple editors and could later be
migrated to the first two.
- Avoid collections with more than 1 editor.
- Create reseed buttons to restore the editor to certain states, to
avoid a proliferation of collections and documents.
- Reduce the complexity of the three auxiliary collections (text, array,
upload), which are rarely or never used and have many fields designed
for tests in the fields suite.
This commit is contained in:
Germán Jabloñski
2025-04-10 20:47:26 -03:00
committed by GitHub
parent 272914c818
commit a66f90ebb6
68 changed files with 2668 additions and 1210 deletions

View File

@@ -1,122 +0,0 @@
'use client'
import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import {
convertLexicalToHTMLAsync,
type HTMLConvertersFunctionAsync,
} from '@payloadcms/richtext-lexical/html-async'
import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
import { useConfig, useDocumentInfo, usePayloadAPI } from '@payloadcms/ui'
import React, { useEffect, useMemo, useState } from 'react'
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes | SerializedBlockNode<any>> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
relationshipBlock: ({ node, nodesToJSX }) => {
return <p>Test</p>
},
},
})
const htmlConverters: HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<any>> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node }) => `<div style="background-color: red;">${node.fields.text}</div>`,
relationshipBlock: () => {
return `<p>Test</p>`
},
},
})
const htmlConvertersAsync: HTMLConvertersFunctionAsync<
DefaultNodeTypes | SerializedBlockNode<any>
> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node }) => `<div style="background-color: red;">${node.fields.text}</div>`,
relationshipBlock: () => {
return `<p>Test</p>`
},
},
})
export const LexicalRendered: React.FC = () => {
const { id, collectionSlug } = useDocumentInfo()
const {
config: {
routes: { api },
serverURL,
},
} = useConfig()
const [{ data }] = usePayloadAPI(`${serverURL}${api}/${collectionSlug}/${id}`, {
initialParams: {
depth: 1,
},
})
const [{ data: unpopulatedData }] = usePayloadAPI(`${serverURL}${api}/${collectionSlug}/${id}`, {
initialParams: {
depth: 0,
},
})
const html: null | string = useMemo(() => {
if (!data.lexicalWithBlocks) {
return null
}
return convertLexicalToHTML({
converters: htmlConverters,
data: data.lexicalWithBlocks as SerializedEditorState,
})
}, [data.lexicalWithBlocks])
const [htmlFromUnpopulatedData, setHtmlFromUnpopulatedData] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
converters: htmlConvertersAsync,
data: unpopulatedData.lexicalWithBlocks as SerializedEditorState,
populate: getRestPopulateFn({
apiURL: `${serverURL}${api}`,
}),
})
setHtmlFromUnpopulatedData(html)
}
void convert()
}, [unpopulatedData.lexicalWithBlocks, api, serverURL])
if (!data.lexicalWithBlocks) {
return null
}
return (
<div>
<h1>Rendered JSX:</h1>
<RichText converters={jsxConverters} data={data.lexicalWithBlocks as SerializedEditorState} />
<h1>Rendered HTML:</h1>
{html && <div dangerouslySetInnerHTML={{ __html: html }} />}
<h1>Rendered HTML 2:</h1>
{htmlFromUnpopulatedData && (
<div dangerouslySetInnerHTML={{ __html: htmlFromUnpopulatedData }} />
)}
<h1>Raw JSON:</h1>
<pre>{JSON.stringify(data.lexicalWithBlocks, null, 2)}</pre>
</div>
)
}

View File

@@ -1,57 +0,0 @@
'use client'
import { $isInlineBlockNode, createClientFeature } from '@payloadcms/richtext-lexical/client'
import { $getSelection } from '@payloadcms/richtext-lexical/lexical'
import { CloseMenuIcon } from '@payloadcms/ui'
import { ModifyInlineBlockPlugin } from './plugin.js'
export const ModifyInlineBlockFeatureClient = createClientFeature({
plugins: [
{
Component: ModifyInlineBlockPlugin,
position: 'normal',
},
],
toolbarFixed: {
groups: [
{
key: 'debug',
items: [
{
ChildComponent: CloseMenuIcon,
key: 'setKeyToDebug',
label: 'Set Key To Debug',
onSelect({ editor }) {
editor.update(() => {
const selection = $getSelection()
// Check if selection consist of 1 node and that its an inlineblocknode
const nodes = selection.getNodes()
if (nodes.length !== 1) {
return
}
const node = nodes[0]
if (!$isInlineBlockNode(node)) {
return
}
const fields = node.getFields()
node.setFields({
blockType: fields.blockType,
id: fields.id,
key: 'value2',
})
})
},
},
],
type: 'buttons',
},
],
},
})

View File

@@ -1,9 +0,0 @@
import { createServerFeature } from '@payloadcms/richtext-lexical'
export const ModifyInlineBlockFeature = createServerFeature({
key: 'ModifyInlineBlockFeature',
feature: {
ClientFeature:
'./collections/Lexical/ModifyInlineBlockFeature/feature.client.js#ModifyInlineBlockFeatureClient',
},
})

View File

@@ -1,7 +0,0 @@
'use client'
import type { PluginComponent } from '@payloadcms/richtext-lexical'
export const ModifyInlineBlockPlugin: PluginComponent = () => {
return null
}

View File

@@ -1,20 +0,0 @@
'use client'
import {
BlockCollapsible,
BlockEditButton,
BlockRemoveButton,
} from '@payloadcms/richtext-lexical/client'
import { useFormFields } from '@payloadcms/ui'
import React from 'react'
export const BlockComponent: React.FC = () => {
const key = useFormFields(([fields]) => fields.key)
return (
<BlockCollapsible>
MY BLOCK COMPONENT. Value: {(key?.value as string) ?? '<no value>'}
Edit: <BlockEditButton />
<BlockRemoveButton />
</BlockCollapsible>
)
}

View File

@@ -1,10 +0,0 @@
import type { BlocksFieldServerComponent } from 'payload'
import { BlockCollapsible } from '@payloadcms/richtext-lexical/client'
import React from 'react'
export const BlockComponentRSC: BlocksFieldServerComponent = (props) => {
const { siblingData } = props
return <BlockCollapsible>Data: {siblingData?.key ?? ''}</BlockCollapsible>
}

View File

@@ -1,10 +0,0 @@
'use client'
import { useFormFields } from '@payloadcms/ui'
import React from 'react'
export const LabelComponent: React.FC = () => {
const key = useFormFields(([fields]) => fields.key)
return <div>{(key?.value as string) ?? '<no value>'}yaya</div>
}

View File

@@ -1,446 +0,0 @@
import type { ArrayField, Block, TextFieldSingleValidation } from 'payload'
import { BlocksFeature, FixedToolbarFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { textFieldsSlug } from '../Text/shared.js'
async function asyncFunction(param: string) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(param?.toUpperCase())
}, 1000)
})
}
export const FilterOptionsBlock: Block = {
slug: 'filterOptionsBlock',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'groupText',
type: 'text',
},
{
name: 'dependsOnDocData',
type: 'relationship',
relationTo: 'text-fields',
filterOptions: ({ data }) => {
if (!data.title) {
return true
}
return {
text: {
equals: data.title,
},
}
},
},
{
name: 'dependsOnSiblingData',
type: 'relationship',
relationTo: 'text-fields',
filterOptions: ({ siblingData }) => {
// @ts-expect-error
if (!siblingData?.groupText) {
return true
}
return {
text: {
equals: (siblingData as any)?.groupText,
},
}
},
},
{
name: 'dependsOnBlockData',
type: 'relationship',
relationTo: 'text-fields',
filterOptions: ({ blockData }) => {
if (!blockData?.text) {
return true
}
return {
text: {
equals: blockData?.text,
},
}
},
},
],
},
],
}
export const ValidationBlock: Block = {
slug: 'validationBlock',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'groupText',
type: 'text',
},
{
name: 'textDependsOnDocData',
type: 'text',
validate: ((value, { data }) => {
if ((data as any)?.title === 'invalid') {
return 'doc title cannot be invalid'
}
return true
}) as TextFieldSingleValidation,
},
{
name: 'textDependsOnSiblingData',
type: 'text',
validate: ((value, { siblingData }) => {
if ((siblingData as any)?.groupText === 'invalid') {
return 'textDependsOnSiblingData sibling field cannot be invalid'
}
}) as TextFieldSingleValidation,
},
{
name: 'textDependsOnBlockData',
type: 'text',
validate: ((value, { blockData }) => {
if ((blockData as any)?.text === 'invalid') {
return 'textDependsOnBlockData sibling field cannot be invalid'
}
}) as TextFieldSingleValidation,
},
],
},
],
}
export const AsyncHooksBlock: Block = {
slug: 'asyncHooksBlock',
fields: [
{
name: 'test1',
label: 'Text',
type: 'text',
hooks: {
afterRead: [
({ value }) => {
return value?.toUpperCase()
},
],
},
},
{
name: 'test2',
label: 'Text',
type: 'text',
hooks: {
afterRead: [
async ({ value }) => {
const valuenew = await asyncFunction(value)
return valuenew
},
],
},
},
],
}
export const BlockColumns = ({ name }: { name: string }): ArrayField => ({
type: 'array',
name,
interfaceName: 'BlockColumns',
admin: {
initCollapsed: true,
},
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'subArray',
type: 'array',
fields: [
{
name: 'requiredText',
type: 'text',
required: true,
},
],
},
],
})
export const ConditionalLayoutBlock: Block = {
fields: [
{
label: 'Layout',
name: 'layout',
type: 'select',
options: ['1', '2', '3'],
defaultValue: '1',
required: true,
},
{
...BlockColumns({ name: 'columns' }),
admin: {
condition: (data, siblingData) => {
return ['1'].includes(siblingData.layout)
},
},
minRows: 1,
maxRows: 1,
},
{
...BlockColumns({ name: 'columns2' }),
admin: {
condition: (data, siblingData) => {
return ['2'].includes(siblingData.layout)
},
},
minRows: 2,
maxRows: 2,
},
{
...BlockColumns({ name: 'columns3' }),
admin: {
condition: (data, siblingData) => {
return ['3'].includes(siblingData.layout)
},
},
minRows: 3,
maxRows: 3,
},
],
slug: 'conditionalLayout',
}
export const TextBlock: Block = {
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
slug: 'textRequired',
}
export const RadioButtonsBlock: Block = {
interfaceName: 'LexicalBlocksRadioButtonsBlock',
fields: [
{
name: 'radioButtons',
type: 'radio',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
{
label: 'Option 3',
value: 'option3',
},
],
},
],
slug: 'radioButtons',
}
export const RichTextBlock: Block = {
fields: [
{
name: 'richTextField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
FixedToolbarFeature(),
BlocksFeature({
blocks: [
{
fields: [
{
name: 'subRichTextField',
type: 'richText',
editor: lexicalEditor({}),
},
{
name: 'subUploadField',
type: 'upload',
relationTo: 'uploads',
},
],
slug: 'lexicalAndUploadBlock',
},
],
}),
],
}),
},
],
slug: 'richTextBlock',
}
export const UploadAndRichTextBlock: Block = {
fields: [
{
name: 'upload',
type: 'upload',
relationTo: 'uploads',
required: true,
},
{
name: 'richText',
type: 'richText',
editor: lexicalEditor(),
},
],
slug: 'uploadAndRichText',
}
export const RelationshipHasManyBlock: Block = {
fields: [
{
name: 'rel',
type: 'relationship',
hasMany: true,
relationTo: [textFieldsSlug, 'uploads'],
required: true,
},
],
slug: 'relationshipHasManyBlock',
}
export const RelationshipBlock: Block = {
fields: [
{
name: 'rel',
type: 'relationship',
relationTo: 'uploads',
required: true,
},
],
slug: 'relationshipBlock',
}
export const SelectFieldBlock: Block = {
fields: [
{
name: 'select',
type: 'select',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
{
label: 'Option 3',
value: 'option3',
},
{
label: 'Option 4',
value: 'option4',
},
{
label: 'Option 5',
value: 'option5',
},
],
},
],
slug: 'select',
}
export const SubBlockBlock: Block = {
slug: 'subBlockLexical',
fields: [
{
name: 'subBlocksLexical',
type: 'blocks',
blocks: [
{
slug: 'contentBlock',
fields: [
{
name: 'richText',
type: 'richText',
required: true,
editor: lexicalEditor(),
},
],
},
{
slug: 'textArea',
fields: [
{
name: 'content',
type: 'textarea',
required: true,
},
],
},
SelectFieldBlock,
],
},
],
}
export const TabBlock: Block = {
slug: 'tabBlock',
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Tab1',
name: 'tab1',
fields: [
{
name: 'text1',
type: 'text',
},
],
},
{
label: 'Tab2',
name: 'tab2',
fields: [
{
name: 'text2',
type: 'text',
},
],
},
],
},
],
}
export const CodeBlock: Block = {
fields: [
{
name: 'code',
type: 'code',
},
],
slug: 'code',
}

View File

@@ -1,61 +0,0 @@
'use client'
import type { SerializedParagraphNode, SerializedTextNode } from '@payloadcms/richtext-lexical'
import { useForm } from '@payloadcms/ui'
import React from 'react'
export const ClearState = ({ fieldName }: { fieldName: string }) => {
const { dispatchFields, fields } = useForm()
const clearState = React.useCallback(() => {
const newState = {
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '',
version: 1,
} as SerializedTextNode,
],
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
textStyle: '',
version: 1,
} as SerializedParagraphNode,
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
}
dispatchFields({
type: 'REPLACE_STATE',
state: {
...fields,
[fieldName]: {
...fields[fieldName],
initialValue: newState,
value: newState,
},
},
})
}, [dispatchFields, fields, fieldName])
return (
<button id={`clear-lexical-${fieldName}`} onClick={clearState} type="button">
Clear State
</button>
)
}

View File

@@ -1,6 +0,0 @@
import { generateLexicalRichText } from './generateLexicalRichText.js'
export const lexicalDocData = {
title: 'Rich Text',
lexicalWithBlocks: generateLexicalRichText(),
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,321 +0,0 @@
import type {
SerializedBlockNode,
SerializedParagraphNode,
SerializedTextNode,
SerializedUploadNode,
TypedEditorState,
} from '@payloadcms/richtext-lexical'
export function generateLexicalRichText(): TypedEditorState<
SerializedBlockNode | SerializedParagraphNode | SerializedTextNode | SerializedUploadNode
> {
return {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Upload Node:',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
textFormat: 0,
version: 1,
},
{
format: '',
type: 'upload',
version: 2,
id: '665d105a91e1c337ba8308dd',
fields: {
caption: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Relationship inside Upload Caption:',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'relationship',
version: 2,
relationTo: 'text-fields',
value: '{{TEXT_DOC_ID}}',
},
],
direction: 'ltr',
},
},
},
relationTo: 'uploads',
value: '{{UPLOAD_DOC_ID}}',
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65298b13db4ef8c744a7faaa',
rel: '{{UPLOAD_DOC_ID}}',
blockName: 'Block Node, with Relationship Field',
blockType: 'relationshipBlock',
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '6565c8668294bf824c24d4a4',
blockName: '',
blockType: 'relationshipHasManyBlock',
rel: [
{
value: '{{TEXT_DOC_ID}}',
relationTo: 'text-fields',
},
{
value: '{{UPLOAD_DOC_ID}}',
relationTo: 'uploads',
},
],
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65298b1ddb4ef8c744a7faab',
richTextField: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
format: '',
type: 'relationship',
version: 2,
relationTo: 'rich-text-fields',
value: '{{RICH_TEXT_DOC_ID}}',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Some text below relationship node 1',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
},
},
blockName: 'Block Node, with RichText Field, with Relationship Node',
blockType: 'richTextBlock',
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65298b2bdb4ef8c744a7faac',
blockName: 'Block Node, with Blocks Field, With RichText Field, With Relationship Node',
blockType: 'subBlockLexical',
subBlocksLexical: [
{
id: '65298b2edb4ef8c744a7faad',
richText: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
format: '',
type: 'relationship',
version: 2,
relationTo: 'text-fields',
value: '{{TEXT_DOC_ID}}',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Some text below relationship node 2',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
},
},
blockType: 'contentBlock',
},
],
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65298b49db4ef8c744a7faae',
upload: '{{UPLOAD_DOC_ID}}',
blockName: 'Block Node, With Upload Field',
blockType: 'uploadAndRichText',
},
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
textFormat: 0,
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65532e49fe515eb112e605a3',
blockName: 'Radio Buttons 1',
blockType: 'radioButtons',
radioButtons: 'option1',
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65532e50fe515eb112e605a4',
blockName: 'Radio Buttons 2',
blockType: 'radioButtons',
radioButtons: 'option1',
},
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
textFormat: 0,
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '65588bfa80fb5a147a378e74',
blockName: '',
blockType: 'conditionalLayout',
layout: '1',
columns: [
{
id: '65588bfb80fb5a147a378e75',
text: 'text in conditionalLayout block',
},
],
},
}, // Do not remove this blocks node. It ensures that validation passes when it's created
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
textFormat: 0,
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '666c9dfd189d72626ea301f9',
blockName: '',
tab1: {
text1: 'Some text1',
},
tab2: {
text2: 'Some text2',
},
blockType: 'tabBlock',
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '666c9e0b189d72626ea301fa',
blockName: '',
blockType: 'code',
code: 'Some code\nhello\nworld',
},
},
],
direction: 'ltr',
},
}
}

View File

@@ -1,408 +0,0 @@
import type { ServerEditorConfig } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import type { Block, BlockSlug, CollectionConfig } from 'payload'
import {
BlocksFeature,
defaultEditorFeatures,
EXPERIMENTAL_TableFeature,
FixedToolbarFeature,
getEnabledNodes,
HeadingFeature,
lexicalEditor,
LinkFeature,
sanitizeServerEditorConfig,
TreeViewFeature,
UploadFeature,
} from '@payloadcms/richtext-lexical'
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
import { lexicalFieldsSlug } from '../../slugs.js'
import {
AsyncHooksBlock,
CodeBlock,
ConditionalLayoutBlock,
FilterOptionsBlock,
RadioButtonsBlock,
RelationshipBlock,
RelationshipHasManyBlock,
RichTextBlock,
SelectFieldBlock,
SubBlockBlock,
TabBlock,
TextBlock,
UploadAndRichTextBlock,
ValidationBlock,
} from './blocks.js'
import { ModifyInlineBlockFeature } from './ModifyInlineBlockFeature/feature.server.js'
export const lexicalBlocks: (Block | BlockSlug)[] = [
ValidationBlock,
FilterOptionsBlock,
AsyncHooksBlock,
RichTextBlock,
TextBlock,
UploadAndRichTextBlock,
SelectFieldBlock,
RelationshipBlock,
RelationshipHasManyBlock,
SubBlockBlock,
RadioButtonsBlock,
ConditionalLayoutBlock,
TabBlock,
CodeBlock,
{
slug: 'myBlock',
admin: {
components: {},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'myBlockWithLabel',
admin: {
components: {
Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'myBlockWithBlock',
admin: {
components: {
Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'BlockRSC',
admin: {
components: {
Block: '/collections/Lexical/blockComponents/BlockComponentRSC.js#BlockComponentRSC',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'myBlockWithBlockAndLabel',
admin: {
components: {
Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent',
Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
]
export const lexicalInlineBlocks: (Block | BlockSlug)[] = [
{
slug: 'AvatarGroup',
interfaceName: 'AvatarGroupBlock',
fields: [
{
name: 'avatars',
type: 'array',
minRows: 1,
maxRows: 6,
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'uploads',
},
],
},
],
},
{
slug: 'myInlineBlock',
admin: {
components: {},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'myInlineBlockWithLabel',
admin: {
components: {
Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'myInlineBlockWithBlock',
admin: {
components: {
Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
{
slug: 'myInlineBlockWithBlockAndLabel',
admin: {
components: {
Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent',
Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent',
},
},
fields: [
{
name: 'key',
label: () => {
return 'Key'
},
type: 'select',
options: ['value1', 'value2', 'value3'],
},
],
},
]
export const getLexicalFieldsCollection: (args: {
blocks: (Block | BlockSlug)[]
inlineBlocks: (Block | BlockSlug)[]
}) => CollectionConfig = ({ blocks, inlineBlocks }) => {
const editorConfig: ServerEditorConfig = {
features: [
...defaultEditorFeatures,
//TestRecorderFeature(),
TreeViewFeature(),
//HTMLConverterFeature(),
FixedToolbarFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
type: 'select',
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
hasMany: true,
label: 'Rel Attribute',
options: ['noopener', 'noreferrer', 'nofollow'],
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
ModifyInlineBlockFeature(),
BlocksFeature({
blocks,
inlineBlocks,
}),
EXPERIMENTAL_TableFeature(),
],
}
return {
slug: lexicalFieldsSlug,
access: {
read: () => true,
},
admin: {
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'lexicalRootEditor',
type: 'richText',
},
{
name: 'lexicalSimple',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
//TestRecorderFeature(),
TreeViewFeature(),
BlocksFeature({
blocks: [
RichTextBlock,
TextBlock,
UploadAndRichTextBlock,
SelectFieldBlock,
RelationshipBlock,
RelationshipHasManyBlock,
SubBlockBlock,
RadioButtonsBlock,
ConditionalLayoutBlock,
],
}),
HeadingFeature({ enabledHeadingSizes: ['h2', 'h4'] }),
],
}),
},
{
type: 'ui',
name: 'clearLexicalState',
admin: {
components: {
Field: {
path: '/collections/Lexical/components/ClearState.js#ClearState',
clientProps: {
fieldName: 'lexicalSimple',
},
},
},
},
},
{
name: 'lexicalWithBlocks',
type: 'richText',
editor: lexicalEditor({
admin: {
hideGutter: false,
},
features: editorConfig.features,
}),
required: true,
},
//{
// name: 'rendered',
// type: 'ui',
// admin: {
// components: {
// Field: './collections/Lexical/LexicalRendered.js#LexicalRendered',
// },
// },
//},
{
name: 'lexicalWithBlocks_markdown',
type: 'textarea',
hooks: {
afterRead: [
async ({ data, req, siblingData }) => {
const yourSanitizedEditorConfig = await sanitizeServerEditorConfig(
editorConfig,
req.payload.config,
)
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: yourSanitizedEditorConfig,
}),
})
const yourEditorState: SerializedEditorState = siblingData.lexicalWithBlocks
try {
headlessEditor.update(
() => {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
},
{ discrete: true },
)
} catch (e) {
/* empty */
}
// Export to markdown
let markdown: string = ''
headlessEditor.getEditorState().read(() => {
markdown = $convertToMarkdownString(
yourSanitizedEditorConfig?.features?.markdownTransformers,
)
})
return markdown
},
],
},
},
],
}
}

View File

@@ -1,18 +0,0 @@
import {
InlineBlockContainer,
InlineBlockEditButton,
InlineBlockLabel,
InlineBlockRemoveButton,
} from '@payloadcms/richtext-lexical/client'
import React from 'react'
export const BlockComponent: React.FC = () => {
return (
<InlineBlockContainer>
<p>Test</p>
<InlineBlockEditButton />
<InlineBlockLabel />
<InlineBlockRemoveButton />
</InlineBlockContainer>
)
}

View File

@@ -1,10 +0,0 @@
'use client'
import { useFormFields } from '@payloadcms/ui'
import React from 'react'
export const LabelComponent: React.FC = () => {
const key = useFormFields(([fields]) => fields.key)
return <div>{(key?.value as string) ?? '<no value>'}yaya</div>
}

View File

@@ -1,2 +0,0 @@
export const loremIpsum =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.'

View File

@@ -1,51 +0,0 @@
import type { CollectionConfig } from 'payload'
import { defaultEditorFeatures, lexicalEditor, LinkFeature } from '@payloadcms/richtext-lexical'
import { lexicalAccessControlSlug } from '../../slugs.js'
export const LexicalAccessControl: CollectionConfig = {
slug: lexicalAccessControlSlug,
access: {
read: () => true,
create: () => false,
},
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: [
...defaultEditorFeatures,
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'block',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
},
],
}),
],
}),
},
],
}

View File

@@ -1,68 +0,0 @@
import type { CollectionConfig } from 'payload'
import { BlocksFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
export const LexicalInBlock: CollectionConfig = {
slug: 'LexicalInBlock',
fields: [
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: [
BlocksFeature({
blocks: [
{
slug: 'blockInLexical',
fields: [
{
name: 'lexicalInBlock',
label: 'My Label',
type: 'richText',
required: true,
editor: lexicalEditor(),
admin: {
description: 'Some Description',
},
},
],
},
],
}),
],
}),
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'lexicalInBlock2',
fields: [
{
name: 'lexical',
type: 'richText',
editor: lexicalEditor({
features: [
BlocksFeature({
inlineBlocks: [
{
slug: 'inlineBlockInLexical',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}),
],
}),
},
],
},
],
},
],
}

View File

@@ -1,44 +0,0 @@
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: 'blockLexicalLocalized',
},
},
],
direction: 'ltr',
},
}
}

View File

@@ -1,103 +0,0 @@
import type { CollectionConfig } from 'payload'
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: 'lexicalBlocksSubLocalized',
type: 'richText',
admin: {
description: 'Non-localized field with localized block subfields',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'blockLexicalLocalized',
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,
},
],
},
],
}),
],
}),
},
{
name: 'lexicalBlocksLocalized',
admin: {
description: 'Localized field with localized block subfields',
},
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
slug: 'blockLexicalLocalized2',
fields: [
{
name: 'textLocalized',
type: 'text',
localized: true,
},
{
name: 'rel',
type: 'relationship',
relationTo: lexicalLocalizedFieldsSlug,
},
],
},
],
}),
],
}),
},
],
}

View File

@@ -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
}

View File

@@ -1,58 +0,0 @@
import { generateSlateRichText } from '../RichText/generateSlateRichText.js'
import { payloadPluginLexicalData } from './generatePayloadPluginLexicalData.js'
export const lexicalMigrateDocData = {
title: 'Rich Text',
lexicalWithLexicalPluginData: payloadPluginLexicalData,
lexicalWithSlateData: [
...generateSlateRichText(),
{
children: [
{
text: 'Some block quote',
},
],
type: 'blockquote',
},
],
arrayWithLexicalField: [
{
lexicalInArrayField: getSimpleLexicalData('array 1'),
},
{
lexicalInArrayField: getSimpleLexicalData('array 2'),
},
],
}
export function getSimpleLexicalData(textContent: string) {
return {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: textContent,
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: 'ltr',
},
}
}

View File

@@ -1,958 +0,0 @@
export const payloadPluginLexicalData = {
words: 49,
preview:
'paragraph text bold italic underline and all subscript superscript code internal link external link…',
comments: [],
characters: 493,
jsonContent: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'paragraph text ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 1,
mode: 'normal',
style: '',
text: 'bold',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 2,
mode: 'normal',
style: '',
text: 'italic',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 8,
mode: 'normal',
style: '',
text: 'underline',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' and ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 11,
mode: 'normal',
style: '',
text: 'all',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 32,
mode: 'normal',
style: '',
text: 'subscript',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 64,
mode: 'normal',
style: '',
text: 'superscript',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 16,
mode: 'normal',
style: '',
text: 'code',
type: 'text',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'internal link',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'link',
version: 2,
attributes: {
newTab: true,
linkType: 'internal',
doc: {
value: '{{TEXT_DOC_ID}}',
relationTo: 'text-fields',
data: {}, // populated data
},
text: 'internal link',
},
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' ',
type: 'text',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'external link',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'link',
version: 2,
attributes: {
newTab: true,
nofollow: false,
url: 'https://fewfwef.de',
linkType: 'custom',
text: 'external link',
},
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ' s. ',
type: 'text',
version: 1,
},
{
detail: 0,
format: 4,
mode: 'normal',
style: '',
text: 'strikethrough',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'heading 1',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'heading',
version: 1,
tag: 'h1',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'heading 2',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'heading',
version: 1,
tag: 'h2',
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'bullet list ',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'item 2',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 2,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'item 3',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 3,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'list',
version: 1,
listType: 'bullet',
start: 1,
tag: 'ul',
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'ordered list',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'item 2',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 2,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'item 3',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 3,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'list',
version: 1,
listType: 'number',
start: 1,
tag: 'ol',
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'check list',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'item 2',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 2,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'item 3',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 3,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'list',
version: 1,
listType: 'check',
start: 1,
tag: 'ul',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'quoteeee',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'quote',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'code block line ',
type: 'code-highlight',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '1',
type: 'code-highlight',
version: 1,
highlightType: 'number',
},
{
type: 'linebreak',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'code block line ',
type: 'code-highlight',
version: 1,
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '2',
type: 'code-highlight',
version: 1,
highlightType: 'number',
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'code',
version: 1,
language: 'javascript',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Upload:',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
type: 'upload',
version: 1,
rawImagePayload: {
value: {
id: '{{UPLOAD_DOC_ID}}',
},
relationTo: 'uploads',
},
caption: {
editorState: {
root: {
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'upload caption',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
},
},
showCaption: true,
data: {}, // populated upload data
},
],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
children: [
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '2x2 table top left',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'tablecell',
version: 1,
colSpan: 1,
rowSpan: 1,
backgroundColor: null,
headerState: 3,
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '2x2 table top right',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'tablecell',
version: 1,
colSpan: 1,
rowSpan: 1,
backgroundColor: null,
headerState: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'tablerow',
version: 1,
},
{
children: [
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '2x2 table bottom left',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'tablecell',
version: 1,
colSpan: 1,
rowSpan: 1,
backgroundColor: null,
headerState: 2,
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '2x2 table bottom right',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'tablecell',
version: 1,
colSpan: 1,
rowSpan: 1,
backgroundColor: null,
headerState: 0,
},
],
direction: null,
format: '',
indent: 0,
type: 'tablerow',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'table',
version: 1,
},
{
rows: [
{
cells: [
{
colSpan: 1,
id: 'kafuj',
json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
type: 'header',
width: null,
},
{
colSpan: 1,
id: 'iussu',
json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
type: 'header',
width: null,
},
],
height: null,
id: 'tnied',
},
{
cells: [
{
colSpan: 1,
id: 'hpnnv',
json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
type: 'header',
width: null,
},
{
colSpan: 1,
id: 'ndteg',
json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
type: 'normal',
width: null,
},
],
height: null,
id: 'rxyey',
},
{
cells: [
{
colSpan: 1,
id: 'rtueq',
json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
type: 'header',
width: null,
},
{
colSpan: 1,
id: 'vrzoi',
json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}',
type: 'normal',
width: null,
},
],
height: null,
id: 'qzglv',
},
],
type: 'tablesheet',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'youtube:',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'youtube',
version: 1,
videoID: '3Nwt3qu0_UY',
},
{
children: [
{
equation: '3+3',
inline: true,
type: 'equation',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'collapsible title',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'collapsible-title',
version: 1,
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'collabsible conteent',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'collapsible-content',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'collapsible-container',
version: 1,
open: true,
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
type: 'horizontalrule',
version: 1,
},
],
direction: 'ltr',
},
},
}

View File

@@ -1,158 +0,0 @@
import type { CollectionConfig } from 'payload'
import {
lexicalEditor,
lexicalHTMLField,
LinkFeature,
TreeViewFeature,
UploadFeature,
} from '@payloadcms/richtext-lexical'
import {
LexicalPluginToLexicalFeature,
SlateToLexicalFeature,
} from '@payloadcms/richtext-lexical/migrate'
import { lexicalMigrateFieldsSlug } from '../../slugs.js'
import { getSimpleLexicalData } from './data.js'
export const LexicalMigrateFields: CollectionConfig = {
slug: lexicalMigrateFieldsSlug,
admin: {
useAsTitle: 'title',
listSearchableFields: ['title', 'richTextLexicalCustomFields'],
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'lexicalWithLexicalPluginData',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LexicalPluginToLexicalFeature({ quiet: true }),
TreeViewFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
],
}),
},
{
name: 'lexicalWithSlateData',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
SlateToLexicalFeature(),
TreeViewFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
],
}),
},
{
name: 'lexicalSimple',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
defaultValue: getSimpleLexicalData('simple'),
},
lexicalHTMLField({ htmlFieldName: 'lexicalSimple_html', lexicalFieldName: 'lexicalSimple' }),
{
name: 'groupWithLexicalField',
type: 'group',
fields: [
{
name: 'lexicalInGroupField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
defaultValue: getSimpleLexicalData('group'),
},
lexicalHTMLField({
htmlFieldName: 'lexicalInGroupField_html',
lexicalFieldName: 'lexicalInGroupField',
}),
],
},
{
name: 'arrayWithLexicalField',
type: 'array',
fields: [
{
name: 'lexicalInArrayField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
},
lexicalHTMLField({
htmlFieldName: 'lexicalInArrayField_html',
lexicalFieldName: 'lexicalInArrayField',
}),
],
},
],
}

View File

@@ -1,6 +0,0 @@
import { payloadPluginLexicalData } from './generatePayloadPluginLexicalData.js'
export const LexicalRichTextDoc = {
title: 'Rich Text',
richTextLexicalWithLexicalPluginData: payloadPluginLexicalData,
}

View File

@@ -1,38 +0,0 @@
import type { CollectionConfig } from 'payload'
import { lexicalEditor, UploadFeature } from '@payloadcms/richtext-lexical'
/**
* Do not change this specific CollectionConfig. Simply having this config in payload used to cause the admin panel to hang.
* Thus, simply having this config in the test suite is enough to test the bug fix and prevent regressions. In case of regression,
* the entire admin panel will hang again and all tests will fail.
*/
export const LexicalObjectReferenceBugCollection: CollectionConfig = {
slug: 'lexicalObjectReferenceBug',
fields: [
{
name: 'lexicalDefault',
type: 'richText',
},
{
name: 'lexicalEditor',
type: 'richText',
editor: lexicalEditor({
features: [
UploadFeature({
collections: {
media: {
fields: [
{
name: 'caption',
type: 'richText',
},
],
},
},
}),
],
}),
},
],
}

View File

@@ -1,38 +0,0 @@
import type { CollectionConfig } from 'payload'
import {
defaultEditorFeatures,
FixedToolbarFeature,
lexicalEditor,
RelationshipFeature,
} from '@payloadcms/richtext-lexical'
import { lexicalRelationshipFieldsSlug } from '../../slugs.js'
export const LexicalRelationshipsFields: CollectionConfig = {
slug: lexicalRelationshipFieldsSlug,
access: {
read: () => true,
},
fields: [
{
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: [
...defaultEditorFeatures,
RelationshipFeature({
enabledCollections: ['array-fields'],
}),
],
}),
},
{
name: 'richText2',
type: 'richText',
editor: lexicalEditor({
features: [...defaultEditorFeatures, RelationshipFeature(), FixedToolbarFeature()],
}),
},
],
}

View File

@@ -1,75 +0,0 @@
import type { Block } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
export const TextBlock: Block = {
fields: [
{
name: 'text',
type: 'text',
required: true,
},
],
slug: 'textRequired',
}
export const UploadAndRichTextBlock: Block = {
fields: [
{
name: 'upload',
type: 'upload',
relationTo: 'uploads',
required: true,
},
{
name: 'richText',
type: 'richText',
editor: lexicalEditor(),
},
],
slug: 'uploadAndRichText',
}
export const RelationshipBlock: Block = {
fields: [
{
name: 'rel',
type: 'relationship',
relationTo: 'uploads',
required: true,
},
],
slug: 'relationshipBlock',
}
export const SelectFieldBlock: Block = {
fields: [
{
name: 'select',
type: 'select',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
{
label: 'Option 3',
value: 'option3',
},
{
label: 'Option 4',
value: 'option4',
},
{
label: 'Option 5',
value: 'option5',
},
],
},
],
slug: 'select',
}

View File

@@ -1,135 +0,0 @@
import { generateLexicalRichText } from './generateLexicalRichText.js'
import { generateSlateRichText } from './generateSlateRichText.js'
export const richTextBlocks = [
{
blockType: 'textBlock',
text: 'Regular text',
},
{
blockType: 'richTextBlockSlate',
text: [
{
children: [
{
text: 'Rich text',
},
],
type: 'h1',
},
],
},
]
export const richTextDocData = {
title: 'Rich Text',
selectHasMany: ['one', 'five'],
richText: generateSlateRichText(),
richTextReadOnly: generateSlateRichText(),
richTextCustomFields: generateSlateRichText(),
lexicalCustomFields: generateLexicalRichText(),
blocks: richTextBlocks,
}
export const richTextBulletsDocData = {
title: 'Bullets and Indentation',
lexicalCustomFields: generateLexicalRichText(),
richText: [
{
type: 'ul',
children: [
{
type: 'li',
children: [
{
children: [
{
text: 'I am semantically connected to my sub-bullets',
},
],
},
{
type: 'ul',
children: [
{
type: 'li',
children: [
{
text: 'I am sub-bullets that are semantically connected to the parent bullet',
},
],
},
],
},
],
},
{
children: [
{
text: 'Normal bullet',
},
],
type: 'li',
},
{
type: 'li',
children: [
{
type: 'ul',
children: [
{
type: 'li',
children: [
{
text: 'I am the old style of sub-bullet',
},
],
},
],
},
],
},
{
type: 'li',
children: [
{
text: 'Another normal bullet',
},
],
},
{
type: 'li',
children: [
{
children: [
{
text: 'This text precedes a nested list',
},
],
},
{
type: 'ul',
children: [
{
type: 'li',
children: [
{
text: 'I am a sub-bullet',
},
],
},
{
type: 'li',
children: [
{
text: 'And I am another sub-bullet',
},
],
},
],
},
],
},
],
},
],
}

View File

@@ -1,452 +0,0 @@
import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import path from 'path'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'
import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
} from '../../../helpers.js'
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../../../helpers/reInitializeDB.js'
import { RESTClient } from '../../../helpers/rest.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const currentFolder = path.dirname(filename)
const dirname = path.resolve(currentFolder, '../../')
const { beforeAll, beforeEach, describe } = test
let client: RESTClient
let page: Page
let serverURL: string
// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' })
describe('Rich Text', () => {
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
;({ serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
}))
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient({ defaultSlug: 'users', serverURL })
await client.login()
await ensureCompilationIsDone({ page, serverURL })
})
async function navigateToRichTextFields() {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields')
await page.goto(url.list)
const linkToDoc = page.locator('.row-1 .cell-title a').first()
await expect(() => expect(linkToDoc).toBeTruthy()).toPass({ timeout: POLL_TOPASS_TIMEOUT })
const linkDocHref = await linkToDoc.getAttribute('href')
await linkToDoc.click()
await page.waitForURL(`**${linkDocHref}`)
}
describe('cell', () => {
test('ensure cells are smaller than 300px in height', async () => {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields')
await page.goto(url.list) // Navigate to rich-text list view
const table = page.locator('.list-controls ~ .table')
const lexicalCell = table.locator('.cell-lexicalCustomFields').first()
const lexicalHtmlCell = table.locator('.cell-lexicalCustomFields_html').first()
const entireRow = table.locator('.row-1').first()
// Make sure each of the 3 above are no larger than 300px in height:
await expect
.poll(async () => (await lexicalCell.boundingBox()).height, {
timeout: POLL_TOPASS_TIMEOUT,
})
.toBeLessThanOrEqual(300)
await expect
.poll(async () => (await lexicalHtmlCell.boundingBox()).height, {
timeout: POLL_TOPASS_TIMEOUT,
})
.toBeLessThanOrEqual(300)
await expect
.poll(async () => (await entireRow.boundingBox()).height, { timeout: POLL_TOPASS_TIMEOUT })
.toBeLessThanOrEqual(300)
})
})
describe('toolbar', () => {
test('should run url validation', async () => {
await navigateToRichTextFields()
// Open link drawer
await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click()
// find the drawer
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
// Fill values and click Confirm
await editLinkModal.locator('#field-text').fill('link text')
await editLinkModal.locator('label[for="field-linkType-custom-2"]').click()
await editLinkModal.locator('#field-url').fill('')
await wait(200)
await editLinkModal.locator('button[type="submit"]').click()
await wait(400)
const errorField = page.locator(
'[id^=drawer_1_rich-text-link-] .render-fields > :nth-child(3)',
)
const hasErrorClass = await errorField.evaluate((el) => el.classList.contains('error'))
expect(hasErrorClass).toBe(true)
})
// TODO: Flaky test flakes consistently in CI: https://github.com/payloadcms/payload/actions/runs/8913431889/job/24478995959?pr=6155
test.skip('should create new url custom link', async () => {
await navigateToRichTextFields()
// Open link drawer
await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click()
// find the drawer
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
await wait(400)
// Fill values and click Confirm
await editLinkModal.locator('#field-text').fill('link text')
await editLinkModal.locator('label[for="field-linkType-custom-2"]').click()
await editLinkModal.locator('#field-url').fill('https://payloadcms.com')
await editLinkModal.locator('button[type="submit"]').click()
await expect(editLinkModal).toBeHidden()
await wait(400)
await saveDocAndAssert(page)
// Remove link from editor body
await page.locator('span >> text="link text"').click()
const popup = page.locator('.popup--active .rich-text-link__popup')
await expect(popup.locator('.rich-text-link__link-label')).toBeVisible()
await popup.locator('.rich-text-link__link-close').click()
await expect(page.locator('span >> text="link text"')).toHaveCount(0)
})
// TODO: Flaky test flakes consistently in CI: https://github.com/payloadcms/payload/actions/runs/8913769794/job/24480056251?pr=6155
test.skip('should create new internal link', async () => {
await navigateToRichTextFields()
// Open link drawer
await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click()
// find the drawer
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
await wait(400)
// Fill values and click Confirm
await editLinkModal.locator('#field-text').fill('link text')
await editLinkModal.locator('label[for="field-linkType-internal-2"]').click()
await editLinkModal.locator('#field-doc .rs__control').click()
await page.keyboard.type('dev@')
await editLinkModal
.locator('#field-doc .rs__menu .rs__option:has-text("dev@payloadcms.com")')
.click()
// await wait(200);
await editLinkModal.locator('button[type="submit"]').click()
await saveDocAndAssert(page)
})
test('should not create new url link when read only', async () => {
await navigateToRichTextFields()
const modalTrigger = page.locator('.rich-text--read-only .rich-text__toolbar button .link')
await expect(modalTrigger).toBeDisabled()
})
test('should only list RTE enabled upload collections in drawer', async () => {
await navigateToRichTextFields()
await wait(1000)
// Open link drawer
await page
.locator('.rich-text__toolbar button:not([disabled]) .upload-rich-text-button')
.first()
.click()
const drawer = page.locator('[id^=list-drawer_1_]')
await expect(drawer).toBeVisible()
// open the list select menu
await page.locator('.list-drawer__select-collection-wrap .rs__control').click()
const menu = page.locator('.list-drawer__select-collection-wrap .rs__menu')
// `uploads-3` has enableRichTextRelationship set to false
await expect(menu).not.toContainText('Uploads3')
})
// TODO: this test can't find the selector for the search filter, but functionality works.
// Need to debug
test.skip('should search correct useAsTitle field after toggling collection in list drawer', async () => {
await navigateToRichTextFields()
// open link drawer
const field = page.locator('#field-richText')
const button = field.locator(
'button.rich-text-relationship__list-drawer-toggler.list-drawer__toggler',
)
await button.click()
// check that the search is on the `name` field of the `text-fields` collection
const drawer = page.locator('[id^=list-drawer_1_]')
await expect(drawer.locator('.search-filter__input')).toHaveAttribute(
'placeholder',
'Search by Text',
)
// change the selected collection to `array-fields`
await page.locator('.list-drawer_select-collection-wrap .rs__control').click()
const menu = page.locator('.list-drawer__select-collection-wrap .rs__menu')
await menu.locator('.rs__option').getByText('Array Field').click()
// check that `id` is now the default search field
await expect(drawer.locator('.search-filter__input')).toHaveAttribute(
'placeholder',
'Search by ID',
)
})
test('should only list RTE enabled collections in link drawer', async () => {
await navigateToRichTextFields()
await wait(1000)
await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click()
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
await wait(1000)
await editLinkModal.locator('label[for="field-linkType-internal-2"]').click()
await editLinkModal.locator('.relationship__wrap .rs__control').click()
const menu = page.locator('.relationship__wrap .rs__menu')
// array-fields has enableRichTextLink set to false
await expect(menu).not.toContainText('Array Fields')
})
test('should only list non-upload collections in relationship drawer', async () => {
await navigateToRichTextFields()
await wait(1000)
// Open link drawer
await page
.locator('.rich-text__toolbar button:not([disabled]) .relationship-rich-text-button')
.first()
.click()
await wait(1000)
// open the list select menu
await page.locator('.list-drawer__select-collection-wrap .rs__control').click()
const menu = page.locator('.list-drawer__select-collection-wrap .rs__menu')
const regex = /\bUploads\b/
await expect(menu).not.toContainText(regex)
})
// TODO: Flaky test in CI. Flake: https://github.com/payloadcms/payload/actions/runs/8914532814/job/24482407114
test.skip('should respect customizing the default fields', async () => {
const linkText = 'link'
const value = 'test value'
await navigateToRichTextFields()
await wait(1000)
const field = page.locator('.rich-text', {
has: page.locator('#field-richTextCustomFields'),
})
// open link drawer
const button = field.locator('button.rich-text__button.link')
await button.click()
await wait(1000)
// fill link fields
const linkDrawer = page.locator('[id^=drawer_1_rich-text-link-]')
const fields = linkDrawer.locator('.render-fields > .field-type')
await fields.locator('#field-text').fill(linkText)
await fields.locator('#field-url').fill('https://payloadcms.com')
const input = fields.locator('#field-fields__customLinkField')
await input.fill(value)
await wait(1000)
// submit link closing drawer
await linkDrawer.locator('button[type="submit"]').click()
const linkInEditor = field.locator(`.rich-text-link >> text="${linkText}"`)
await wait(300)
await saveDocAndAssert(page)
// open modal again
await linkInEditor.click()
const popup = page.locator('.popup--active .rich-text-link__popup')
await expect(popup).toBeVisible()
await popup.locator('.rich-text-link__link-edit').click()
const linkDrawer2 = page.locator('[id^=drawer_1_rich-text-link-]')
const fields2 = linkDrawer2.locator('.render-fields > .field-type')
const input2 = fields2.locator('#field-fields__customLinkField')
await expect(input2).toHaveValue(value)
})
})
describe('editor', () => {
test('should populate url link', async () => {
await navigateToRichTextFields()
await wait(500)
// Open link popup
await page.locator('#field-richText span >> text="render links"').click()
const popup = page.locator('.popup--active .rich-text-link__popup')
await expect(popup).toBeVisible()
await expect(popup.locator('a')).toHaveAttribute('href', 'https://payloadcms.com')
// Open the drawer
await popup.locator('.rich-text-link__link-edit').click()
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
// Check the drawer values
const textField = editLinkModal.locator('#field-text')
await expect(textField).toHaveValue('render links')
await wait(1000)
// Close the drawer
await editLinkModal.locator('button[type="submit"]').click()
await expect(editLinkModal).toBeHidden()
})
test('should populate relationship link', async () => {
await navigateToRichTextFields()
// Open link popup
await page.locator('#field-richText span >> text="link to relationships"').click()
const popup = page.locator('.popup--active .rich-text-link__popup')
await expect(popup).toBeVisible()
await expect(popup.locator('a')).toHaveAttribute(
'href',
/\/admin\/collections\/array-fields\/.*/,
)
// Open the drawer
await popup.locator('.rich-text-link__link-edit').click()
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
// Check the drawer values
const textField = editLinkModal.locator('#field-text')
await expect(textField).toHaveValue('link to relationships')
})
test('should open upload drawer and render custom relationship fields', async () => {
await navigateToRichTextFields()
const field = page.locator('#field-richText')
const button = field.locator('button.rich-text-upload__upload-drawer-toggler')
await button.click()
const documentDrawer = page.locator('[id^=drawer_1_upload-drawer-]')
await expect(documentDrawer).toBeVisible()
const caption = documentDrawer.locator('#field-caption')
await expect(caption).toBeVisible()
})
test('should open upload document drawer from read-only field', async () => {
await navigateToRichTextFields()
const field = page.locator('#field-richTextReadOnly')
const button = field.locator(
'button.rich-text-upload__doc-drawer-toggler.doc-drawer__toggler',
)
await button.click()
const documentDrawer = page.locator('[id^=doc-drawer_uploads_1_]')
await expect(documentDrawer).toBeVisible()
})
test('should open relationship document drawer from read-only field', async () => {
await navigateToRichTextFields()
const field = page.locator('#field-richTextReadOnly')
const button = field.locator(
'button.rich-text-relationship__doc-drawer-toggler.doc-drawer__toggler',
)
await button.click()
const documentDrawer = page.locator('[id^=doc-drawer_text-fields_1_]')
await expect(documentDrawer).toBeVisible()
})
test('should populate new links', async () => {
await navigateToRichTextFields()
await wait(1000)
// Highlight existing text
const headingElement = page.locator(
'#field-richText h1 >> text="Hello, I\'m a rich text field."',
)
await headingElement.selectText()
await wait(500)
// click the toolbar link button
await page.locator('.rich-text__toolbar button:not([disabled]) .link').first().click()
// find the drawer and confirm the values
const editLinkModal = page.locator('[id^=drawer_1_rich-text-link-]')
await expect(editLinkModal).toBeVisible()
const textField = editLinkModal.locator('#field-text')
await expect(textField).toHaveValue("Hello, I'm a rich text field.")
})
test('should not take value from previous block', async () => {
await navigateToRichTextFields()
await page.locator('#field-blocks').scrollIntoViewIfNeeded()
await expect(page.locator('#field-blocks__0__text')).toBeVisible()
await expect(page.locator('#field-blocks__0__text')).toHaveValue('Regular text')
await wait(500)
const editBlock = page.locator('#blocks-row-0 .popup-button')
await editBlock.click()
const removeButton = page.locator('#blocks-row-0').getByRole('button', { name: 'Remove' })
await expect(removeButton).toBeVisible()
await wait(500)
await removeButton.click()
const richTextField = page.locator('#field-blocks__0__text')
await expect(richTextField).toBeVisible()
const richTextValue = await richTextField.innerText()
expect(richTextValue).toContain('Rich text')
})
})
})

View File

@@ -1,318 +0,0 @@
import { textFieldsSlug } from '../../slugs.js'
import { loremIpsum } from './loremIpsum.js'
export function generateLexicalRichText() {
return {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: "Hello, I'm a rich text field.",
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: 'center',
indent: 0,
type: 'heading',
version: 1,
tag: 'h1',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'I can do all kinds of fun stuff like ',
type: 'text',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'render links',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
id: '665d10938106ab380c7f3730',
type: 'link',
version: 2,
fields: {
url: 'https://payloadcms.com',
newTab: true,
linkType: 'custom',
},
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ', ',
type: 'text',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'link to relationships',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
id: '665d10938106ab380c7f3730',
type: 'link',
version: 2,
fields: {
url: 'https://',
doc: {
value: '{{TEXT_DOC_ID}}',
relationTo: textFieldsSlug,
},
newTab: false,
linkType: 'internal',
},
},
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: ', and store nested relationship fields:',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'relationship',
version: 2,
value: '{{TEXT_DOC_ID}}',
relationTo: 'text-fields',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'You can build your own elements, too.',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: "It's built with Lexical",
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'It stores content as JSON so you can use it wherever you need',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 2,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: "It's got a great editing experience for non-technical users",
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'listitem',
version: 1,
value: 3,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'list',
version: 1,
listType: 'bullet',
start: 1,
tag: 'ul',
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'And a whole lot more.',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'upload',
version: 2,
id: '665d10938106ab380c7f372f',
relationTo: 'uploads',
value: '{{UPLOAD_DOC_ID}}',
fields: {
caption: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
...[...Array(4)].map(() => ({
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: loremIpsum,
type: 'text',
version: 1,
},
],
})),
],
direction: 'ltr',
},
},
},
},
{
children: [],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: 'ltr',
},
}
}

View File

@@ -1,148 +0,0 @@
import { loremIpsum } from './loremIpsum.js'
export function generateSlateRichText() {
return [
{
children: [
{
text: "Hello, I'm a rich text field.",
},
],
type: 'h1',
textAlign: 'center',
},
{
children: [
{
text: 'I can do all kinds of fun stuff like ',
},
{
type: 'link',
url: 'https://payloadcms.com',
newTab: true,
children: [
{
text: 'render links',
},
],
},
{
text: ', ',
},
{
type: 'link',
linkType: 'internal',
doc: {
value: '{{ARRAY_DOC_ID}}',
relationTo: 'array-fields',
},
fields: {},
children: [
{
text: 'link to relationships',
},
],
},
{
text: ', and store nested relationship fields:',
},
],
},
{
children: [
{
text: '',
},
],
type: 'relationship',
value: {
id: '{{TEXT_DOC_ID}}',
},
relationTo: 'text-fields',
},
{
children: [
{
text: 'You can build your own elements, too.',
},
],
},
{
type: 'ul',
children: [
{
children: [
{
text: "It's built with SlateJS",
},
],
type: 'li',
},
{
type: 'li',
children: [
{
text: 'It stores content as JSON so you can use it wherever you need',
},
],
},
{
type: 'li',
children: [
{
text: "It's got a great editing experience for non-technical users",
},
],
},
],
},
{
children: [
{
text: 'And a whole lot more.',
},
],
},
{
children: [
{
text: '',
},
],
type: 'upload',
value: {
id: '{{UPLOAD_DOC_ID}}',
},
relationTo: 'uploads',
fields: {
caption: [
...[...Array(4)].map(() => {
return {
children: [
{
text: loremIpsum,
},
],
}
}),
],
},
},
{
children: [
{
text: '',
},
],
},
...[...Array(2)].map(() => {
return {
children: [
{
text: loremIpsum,
},
],
}
}),
]
}

View File

@@ -1,324 +0,0 @@
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
HTMLConverterFeature,
lexicalEditor,
lexicalHTML,
LinkFeature,
TreeViewFeature,
UploadFeature,
} from '@payloadcms/richtext-lexical'
import { slateEditor } from '@payloadcms/richtext-slate'
import { richTextFieldsSlug } from '../../slugs.js'
import { RelationshipBlock, SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks.js'
const RichTextFields: CollectionConfig = {
slug: richTextFieldsSlug,
admin: {
useAsTitle: 'title',
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'lexicalCustomFields',
type: 'richText',
required: true,
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
TreeViewFeature(),
HTMLConverterFeature({}),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
BlocksFeature({
blocks: [TextBlock, UploadAndRichTextBlock, SelectFieldBlock, RelationshipBlock],
}),
],
}),
},
lexicalHTML('lexicalCustomFields', { name: 'lexicalCustomFields_html' }),
{
name: 'lexical',
type: 'richText',
admin: {
description: 'This rich text field uses the lexical editor.',
},
defaultValue: {
root: {
children: [
{
children: [
{
text: 'This is a paragraph.',
type: 'text',
},
],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'root',
version: 1,
},
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures, TreeViewFeature()],
}),
},
{
name: 'selectHasMany',
hasMany: true,
type: 'select',
admin: {
description:
'This select field is rendered here to ensure its options dropdown renders above the rich text toolbar.',
},
options: [
{
label: 'Value One',
value: 'one',
},
{
label: 'Value Two',
value: 'two',
},
{
label: 'Value Three',
value: 'three',
},
{
label: 'Value Four',
value: 'four',
},
{
label: 'Value Five',
value: 'five',
},
{
label: 'Value Six',
value: 'six',
},
],
},
{
name: 'richText',
type: 'richText',
editor: slateEditor({
admin: {
elements: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'textAlign',
'indent',
'link',
'relationship',
'upload',
],
link: {
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
},
upload: {
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: slateEditor({}),
},
],
},
},
},
},
}),
required: true,
},
{
name: 'richTextCustomFields',
type: 'richText',
editor: slateEditor({
admin: {
elements: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'indent',
'link',
'relationship',
'upload',
],
link: {
fields: ({ defaultFields }) => {
return [
...defaultFields,
{
label: 'Custom',
name: 'customLinkField',
type: 'text',
},
]
},
},
upload: {
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: slateEditor({}),
},
],
},
},
},
},
}),
},
{
name: 'richTextReadOnly',
type: 'richText',
admin: {
readOnly: true,
},
editor: slateEditor({
admin: {
elements: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'ul',
'ol',
'indent',
'link',
'relationship',
'upload',
],
link: {
fields: [
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
},
upload: {
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
},
],
},
},
},
},
}),
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'textBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
{
slug: 'richTextBlockSlate',
fields: [
{
editor: slateEditor({}),
name: 'text',
type: 'richText',
},
],
},
],
},
],
}
export default RichTextFields

View File

@@ -1,2 +0,0 @@
export const loremIpsum =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.'