Files
payloadcms/test/lexical/collections/Lexical/index.ts
Alessio Gravili 66f5d1429d fix(richtext-lexical): richtext field duplicates description custom component (#13880)
The lexical field component was accidentally rendering the description
component twice.

Fixes https://github.com/payloadcms/payload/issues/13644
2025-09-22 14:39:55 -04:00

420 lines
9.8 KiB
TypeScript

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,
NoBlockNameBlock,
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,
NoBlockNameBlock,
{
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',
admin: {
description: 'A simple lexical field',
},
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',
admin: {
components: {
Description: '/collections/Lexical/components/Description.js#Description',
},
description: 'Should not be rendered',
},
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
},
],
},
},
],
}
}