fix(richtext-*): RichText Cell components (#5174)

* fix(richtext-*): RichText Cell components

* better code
This commit is contained in:
Alessio Gravili
2024-02-26 15:37:50 -05:00
committed by GitHub
parent 75c4b4f234
commit a0cf2ea56b
8 changed files with 53 additions and 69 deletions

View File

@@ -1,33 +0,0 @@
'use client'
import type { CellComponentProps, RichTextAdapter, RichTextField } from 'payload/types'
import React, { useMemo } from 'react'
export const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.cellData.editor
const isLazy = 'LazyCellComponent' in editor
const ImportedCellComponent: React.FC<any> = useMemo(() => {
return isLazy
? React.lazy(() => {
return editor.LazyCellComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
})
: null
}, [editor, isLazy])
if (isLazy) {
return (
ImportedCellComponent && (
<React.Suspense>
<ImportedCellComponent {...props} />
</React.Suspense>
)
)
}
return <editor.CellComponent {...props} />
}

View File

@@ -6,7 +6,6 @@ import { DateCell } from './Date'
import { FileCell } from './File' import { FileCell } from './File'
import { JSONCell } from './JSON' import { JSONCell } from './JSON'
import { RelationshipCell } from './Relationship' import { RelationshipCell } from './Relationship'
import { RichTextCell } from './Richtext'
import { SelectCell } from './Select' import { SelectCell } from './Select'
import { TextareaCell } from './Textarea' import { TextareaCell } from './Textarea'
@@ -20,7 +19,6 @@ export default {
json: JSONCell, json: JSONCell,
radio: SelectCell, radio: SelectCell,
relationship: RelationshipCell, relationship: RelationshipCell,
richText: RichTextCell,
select: SelectCell, select: SelectCell,
textarea: TextareaCell, textarea: TextareaCell,
upload: RelationshipCell, upload: RelationshipCell,

View File

@@ -13,6 +13,7 @@ import { CodeCell } from './fields/Code'
export const DefaultCell: React.FC<CellProps> = (props) => { export const DefaultCell: React.FC<CellProps> = (props) => {
const { const {
name, name,
CellComponentOverride,
className: classNameFromProps, className: classNameFromProps,
fieldType, fieldType,
isFieldAffectingData, isFieldAffectingData,
@@ -76,7 +77,8 @@ export const DefaultCell: React.FC<CellProps> = (props) => {
) )
} }
let CellComponent: React.FC<CellComponentProps> = cellData && cellComponents[fieldType] let CellComponent: React.FC<CellComponentProps> =
cellData && (CellComponentOverride ? CellComponentOverride : cellComponents[fieldType])
if (!CellComponent) { if (!CellComponent) {
if (customCellContext.uploadConfig && isFieldAffectingData && name === 'filename') { if (customCellContext.uploadConfig && isFieldAffectingData && name === 'filename') {

View File

@@ -10,6 +10,13 @@ import type {
} from '../../fields/config/types' } from '../../fields/config/types'
export type CellProps = { export type CellProps = {
/**
* A custom component to override the default cell component. If this is not set, the React component will be
* taken from cellComponents based on the field type.
*
* This is used to provide the RichText cell component for the RichText field.
*/
CellComponentOverride?: React.ComponentType<CellComponentProps>
blocks?: { blocks?: {
labels: BlockField['labels'] labels: BlockField['labels']
slug: string slug: string

View File

@@ -1,14 +1,10 @@
'use client' 'use client'
import type { CellComponentProps, RichTextField } from 'payload/types' import type { CellComponentProps } from 'payload/types'
import React from 'react' import React from 'react'
import type { AdapterArguments } from '../types' const RichTextCell: React.FC<CellComponentProps<any[]>> = ({ cellData }) => {
const flattenedText = cellData?.map((i) => i?.children?.map((c) => c.text)).join(' ')
const RichTextCell: React.FC<
CellComponentProps<RichTextField<any[], AdapterArguments, AdapterArguments>, any>
> = ({ data }) => {
const flattenedText = data?.map((i) => i?.children?.map((c) => c.text)).join(' ')
// Limiting the number of characters shown is done in a CSS rule // Limiting the number of characters shown is done in a CSS rule
return <span>{flattenedText}</span> return <span>{flattenedText}</span>

View File

@@ -1,6 +1,5 @@
import type { RichTextAdapter } from 'payload/types' import type { RichTextAdapter } from 'payload/types'
import { withMergedProps } from '@payloadcms/ui/utilities'
import { withNullableJSONSchemaType } from 'payload/utilities' import { withNullableJSONSchemaType } from 'payload/utilities'
import type { AdapterArguments } from './types' import type { AdapterArguments } from './types'
@@ -14,22 +13,16 @@ import { getGenerateSchemaMap } from './generateSchemaMap'
export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> { export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> {
return { return {
CellComponent: withMergedProps({ CellComponent: RichTextCell,
Component: RichTextCell, FieldComponent: RichTextField,
toMergeIntoProps: args,
}),
FieldComponent: withMergedProps({
Component: RichTextField,
toMergeIntoProps: args,
}),
generateComponentMap: getGenerateComponentMap(args), generateComponentMap: getGenerateComponentMap(args),
generateSchemaMap: getGenerateSchemaMap(args), generateSchemaMap: getGenerateSchemaMap(args),
outputSchema: ({ isRequired }) => { outputSchema: ({ isRequired }) => {
return { return {
type: withNullableJSONSchemaType('array', isRequired),
items: { items: {
type: 'object', type: 'object',
}, },
type: withNullableJSONSchemaType('array', isRequired),
} }
}, },
populationPromise({ populationPromise({

View File

@@ -80,7 +80,7 @@ export const mapFields = (args: {
const labelProps: LabelProps = { const labelProps: LabelProps = {
htmlFor: 'TODO', htmlFor: 'TODO',
// TODO: fix types // TODO: fix types
// @ts-ignore-next-line // @ts-expect-error-next-line
label: 'label' in field ? field.label : null, label: 'label' in field ? field.label : null,
required: 'required' in field ? field.required : undefined, required: 'required' in field ? field.required : undefined,
} }
@@ -153,10 +153,10 @@ export const mapFields = (args: {
}) })
const reducedBlock: ReducedBlock = { const reducedBlock: ReducedBlock = {
slug: block.slug,
imageAltText: block.imageAltText, imageAltText: block.imageAltText,
imageURL: block.imageURL, imageURL: block.imageURL,
labels: block.labels, labels: block.labels,
slug: block.slug,
subfields: blockFieldMap, subfields: blockFieldMap,
} }
@@ -245,8 +245,8 @@ export const mapFields = (args: {
blocks: blocks:
'blocks' in field && 'blocks' in field &&
field.blocks.map((b) => ({ field.blocks.map((b) => ({
labels: b.labels,
slug: b.slug, slug: b.slug,
labels: b.labels,
})), })),
dateDisplayFormat: 'date' in field.admin ? field.admin.date.displayFormat : undefined, dateDisplayFormat: 'date' in field.admin ? field.admin.date.displayFormat : undefined,
fieldType: field.type, fieldType: field.type,
@@ -259,13 +259,18 @@ export const mapFields = (args: {
options: 'options' in field ? field.options : undefined, options: 'options' in field ? field.options : undefined,
} }
/**
* Handle RichText Field Components, Cell Components, and component maps
*/
if (field.type === 'richText' && 'editor' in field) { if (field.type === 'richText' && 'editor' in field) {
let RichTextComponent let RichTextFieldComponent
let RichTextCellComponent
const isLazy = 'LazyFieldComponent' in field.editor const isLazyField = 'LazyFieldComponent' in field.editor
const isLazyCell = 'LazyCellComponent' in field.editor
if (isLazy) { if (isLazyField) {
RichTextComponent = React.lazy(() => { RichTextFieldComponent = React.lazy(() => {
return 'LazyFieldComponent' in field.editor return 'LazyFieldComponent' in field.editor
? field.editor.LazyFieldComponent().then((resolvedComponent) => ({ ? field.editor.LazyFieldComponent().then((resolvedComponent) => ({
default: resolvedComponent, default: resolvedComponent,
@@ -273,22 +278,39 @@ export const mapFields = (args: {
: null : null
}) })
} else if ('FieldComponent' in field.editor) { } else if ('FieldComponent' in field.editor) {
RichTextComponent = field.editor.FieldComponent RichTextFieldComponent = field.editor.FieldComponent
}
if (isLazyCell) {
RichTextCellComponent = React.lazy(() => {
return 'LazyCellComponent' in field.editor
? field.editor.LazyCellComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
: null
})
} else if ('CellComponent' in field.editor) {
RichTextCellComponent = field.editor.CellComponent
} }
if (typeof field.editor.generateComponentMap === 'function') { if (typeof field.editor.generateComponentMap === 'function') {
const result = field.editor.generateComponentMap({ config, schemaPath: path }) const result = field.editor.generateComponentMap({ config, schemaPath: path })
// @ts-ignore-next-line // TODO: the `richTextComponentMap` is not found on the union type // @ts-expect-error-next-line // TODO: the `richTextComponentMap` is not found on the union type
fieldComponentProps.richTextComponentMap = result fieldComponentProps.richTextComponentMap = result
} }
if (RichTextComponent) { if (RichTextFieldComponent) {
Field = <RichTextComponent {...fieldComponentProps} /> Field = <RichTextFieldComponent {...fieldComponentProps} />
}
if (RichTextCellComponent) {
cellComponentProps.CellComponentOverride = RichTextCellComponent
} }
} }
const reducedField: MappedField = { const reducedField: MappedField = {
name: 'name' in field ? field.name : '', name: 'name' in field ? field.name : '',
type: field.type,
Cell: ( Cell: (
<RenderCustomComponent <RenderCustomComponent
CustomComponent={field.admin?.components?.Cell} CustomComponent={field.admin?.components?.Cell}
@@ -326,7 +348,6 @@ export const mapFields = (args: {
readOnly, readOnly,
subfields: nestedFieldMap, subfields: nestedFieldMap,
tabs, tabs,
type: field.type,
} }
if (FieldComponent) { if (FieldComponent) {
@@ -344,6 +365,7 @@ export const mapFields = (args: {
if (!hasID) { if (!hasID) {
result.push({ result.push({
name: 'id', name: 'id',
type: 'text',
Cell: typeof DefaultCell === 'function' ? <DefaultCell name="id" /> : null, Cell: typeof DefaultCell === 'function' ? <DefaultCell name="id" /> : null,
Field: <HiddenInput name="id" />, Field: <HiddenInput name="id" />,
Heading: <SortColumn label="ID" name="id" />, Heading: <SortColumn label="ID" name="id" />,
@@ -355,7 +377,6 @@ export const mapFields = (args: {
readOnly: false, readOnly: false,
subfields: [], subfields: [],
tabs: [], tabs: [],
type: 'text',
}) })
} }

View File

@@ -225,7 +225,7 @@ describe('Lexical', () => {
}) })
}) })
describe('converters and migrations', () => { describe('converters and migrations', () => {
it('hTMLConverter: should output correct HTML for top-level lexical field', async () => { it('htmlConverter: should output correct HTML for top-level lexical field', async () => {
const lexicalDoc: LexicalMigrateField = ( const lexicalDoc: LexicalMigrateField = (
await payload.find({ await payload.find({
collection: lexicalMigrateFieldsSlug, collection: lexicalMigrateFieldsSlug,
@@ -241,7 +241,7 @@ describe('Lexical', () => {
const htmlField: string = lexicalDoc?.lexicalSimple_html const htmlField: string = lexicalDoc?.lexicalSimple_html
expect(htmlField).toStrictEqual('<p>simple</p>') expect(htmlField).toStrictEqual('<p>simple</p>')
}) })
it('hTMLConverter: should output correct HTML for lexical field nested in group', async () => { it('htmlConverter: should output correct HTML for lexical field nested in group', async () => {
const lexicalDoc: LexicalMigrateField = ( const lexicalDoc: LexicalMigrateField = (
await payload.find({ await payload.find({
collection: lexicalMigrateFieldsSlug, collection: lexicalMigrateFieldsSlug,
@@ -257,7 +257,7 @@ describe('Lexical', () => {
const htmlField: string = lexicalDoc?.groupWithLexicalField?.lexicalInGroupField_html const htmlField: string = lexicalDoc?.groupWithLexicalField?.lexicalInGroupField_html
expect(htmlField).toStrictEqual('<p>group</p>') expect(htmlField).toStrictEqual('<p>group</p>')
}) })
it('hTMLConverter: should output correct HTML for lexical field nested in array', async () => { it('htmlConverter: should output correct HTML for lexical field nested in array', async () => {
const lexicalDoc: LexicalMigrateField = ( const lexicalDoc: LexicalMigrateField = (
await payload.find({ await payload.find({
collection: lexicalMigrateFieldsSlug, collection: lexicalMigrateFieldsSlug,