fix(richtext-*): RichText Cell components (#5174)
* fix(richtext-*): RichText Cell components * better code
This commit is contained in:
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user