chore: scaffolds ssr rte

This commit is contained in:
Jacob Fletcher
2024-02-21 13:56:04 -05:00
parent 122e8ac9d6
commit bc0525589c
23 changed files with 476 additions and 201 deletions

View File

@@ -13,6 +13,10 @@ export const sanitizeField = (f) => {
field.fields = sanitizeFields(field.fields) field.fields = sanitizeFields(field.fields)
} }
if ('editor' in field) {
delete field.editor
}
if ('blocks' in field) { if ('blocks' in field) {
field.blocks = field.blocks.map((block) => { field.blocks = field.blocks.map((block) => {
const sanitized = { ...block } const sanitized = { ...block }
@@ -94,6 +98,7 @@ export const createClientConfig = async (
delete clientConfig.endpoints delete clientConfig.endpoints
delete clientConfig.db delete clientConfig.db
delete clientConfig.editor
'localization' in clientConfig && 'localization' in clientConfig &&
clientConfig.localization && clientConfig.localization &&

View File

@@ -26,6 +26,7 @@ type RichTextAdapterBase<
incomingEditorState: Value incomingEditorState: Value
siblingDoc: Record<string, unknown> siblingDoc: Record<string, unknown>
}) => Promise<void> | null }) => Promise<void> | null
generateComponentMap: () => Map<string, React.ReactNode> | Promise<Map<string, React.ReactNode>>
outputSchema?: ({ outputSchema?: ({
field, field,
isRequired, isRequired,

View File

@@ -5,22 +5,23 @@ import type { HistoryEditor } from 'slate-history'
import type { ReactEditor } from 'slate-react' import type { ReactEditor } from 'slate-react'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { import { useEditDepth, useField, useTranslation } from '@payloadcms/ui'
Error,
FieldDescription,
Label,
useEditDepth,
useField,
useTranslation,
} from '@payloadcms/ui'
import isHotkey from 'is-hotkey' import isHotkey from 'is-hotkey'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Node, Element as SlateElement, Text, Transforms, createEditor } from 'slate' import { Node, Element as SlateElement, Text, Transforms, createEditor } from 'slate'
import { withHistory } from 'slate-history' import { withHistory } from 'slate-history'
import { Editable, Slate, withReact } from 'slate-react' import { Editable, Slate, withReact } from 'slate-react'
import type { ElementNode, FieldProps, RichTextElement, RichTextLeaf, TextNode } from '../types' import type { FormFieldBase } from '../../../ui/src/forms/fields/shared'
import type {
ElementNode,
RichTextCustomLeaf,
RichTextElement,
RichTextLeaf,
TextNode,
} from '../types'
import { withCondition } from '../../../ui/src/forms/withCondition'
import { defaultRichTextValue } from '../data/defaultValue' import { defaultRichTextValue } from '../data/defaultValue'
import { richTextValidate } from '../data/validation' import { richTextValidate } from '../data/validation'
import elementTypes from './elements' import elementTypes from './elements'
@@ -33,6 +34,8 @@ import toggleLeaf from './leaves/toggle'
import mergeCustomFunctions from './mergeCustomFunctions' import mergeCustomFunctions from './mergeCustomFunctions'
import withEnterBreakOut from './plugins/withEnterBreakOut' import withEnterBreakOut from './plugins/withEnterBreakOut'
import withHTML from './plugins/withHTML' import withHTML from './plugins/withHTML'
import { LeafButtonProvider } from './providers/LeafButtonProvider'
import { LeafProvider } from './providers/LeafProvider'
const defaultElements: RichTextElement[] = [ const defaultElements: RichTextElement[] = [
'h1', 'h1',
@@ -60,127 +63,163 @@ declare module 'slate' {
} }
} }
const RichText: React.FC<FieldProps> = (props) => { const RichText: React.FC<
FormFieldBase & {
name: string
richTextComponentMap: Map<string, React.ReactNode>
}
> = (props) => {
const { const {
name, name,
admin: { Description,
className, Error,
condition, Label,
description, className,
hideGutter,
placeholder,
readOnly,
style,
width,
} = {
className: undefined,
condition: undefined,
description: undefined,
hideGutter: undefined,
placeholder: undefined,
readOnly: undefined,
style: undefined,
width: undefined,
},
admin,
defaultValue: defaultValueFromProps,
label,
path: pathFromProps, path: pathFromProps,
placeholder,
readOnly,
required, required,
richTextComponentMap,
style,
validate = richTextValidate, validate = richTextValidate,
width,
} = props } = props
const elements: RichTextElement[] = admin?.elements || defaultElements const [leaves] = useState(() => {
const leaves: RichTextLeaf[] = admin?.leaves || defaultLeaves const enabledLeaves: Record<
string,
{
Button: React.ReactNode
Leaf: React.ReactNode
name: string
}
> = {}
const path = pathFromProps || name for (const [key, value] of richTextComponentMap) {
if (key.startsWith('leaf.button') || key.startsWith('leaf.component')) {
const leafName = key.replace('leaf.button', '').replace('leaf.component', '')
if (!enabledLeaves[leafName]) {
enabledLeaves[leafName] = {
name: leafName,
Button: null,
Leaf: null,
}
}
if (key.startsWith('leaf.button')) enabledLeaves[leafName].Button = value
if (key.startsWith('leaf.component')) enabledLeaves[leafName].Leaf = value
}
}
return enabledLeaves
})
const elements: RichTextElement[] = defaultElements
const { i18n } = useTranslation() const { i18n } = useTranslation()
const [loaded, setLoaded] = useState(false)
const [enabledElements, setEnabledElements] = useState({})
const [enabledLeaves, setEnabledLeaves] = useState({})
const editorRef = useRef(null) const editorRef = useRef(null)
const toolbarRef = useRef(null) const toolbarRef = useRef(null)
const drawerDepth = useEditDepth() const drawerDepth = useEditDepth()
const drawerIsOpen = drawerDepth > 1 const drawerIsOpen = drawerDepth > 1
const renderElement = useCallback( const memoizedValidate = useCallback(
({ attributes, children, element }) => { (value, validationOptions) => {
const matchedElement = enabledElements[element.type] if (typeof validate === 'function') {
const Element = matchedElement?.Element return validate(value, { ...validationOptions, required })
let attr = { ...attributes }
// this converts text alignment to margin when dealing with void elements
if (element.textAlign) {
if (element.type === 'relationship' || element.type === 'upload') {
switch (element.textAlign) {
case 'left':
attr = { ...attr, style: { marginRight: 'auto' } }
break
case 'right':
attr = { ...attr, style: { marginLeft: 'auto' } }
break
case 'center':
attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
break
default:
attr = { ...attr, style: { textAlign: element.textAlign } }
break
}
} else if (element.type === 'li') {
switch (element.textAlign) {
case 'right':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
break
case 'center':
attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } }
break
case 'left':
default:
attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } }
break
}
} else {
attr = { ...attr, style: { textAlign: element.textAlign } }
}
} }
if (Element) {
const el = (
<Element
attributes={attr}
editorRef={editorRef}
element={element}
fieldProps={props}
path={path}
>
{children}
</Element>
)
return el
}
return <div {...attr}>{children}</div>
}, },
[enabledElements, path, props], [validate, required],
) )
const { initialValue, path, schemaPath, setValue, showError, value } = useField({
path: pathFromProps || name,
validate: memoizedValidate,
})
const renderElement = useCallback(({ attributes, children, element }) => {
// const matchedElement = enabledElements[element.type]
// const Element = matchedElement?.Element
const attr = { ...attributes }
// // this converts text alignment to margin when dealing with void elements
// if (element.textAlign) {
// if (element.type === 'relationship' || element.type === 'upload') {
// switch (element.textAlign) {
// case 'left':
// attr = { ...attr, style: { marginRight: 'auto' } }
// break
// case 'right':
// attr = { ...attr, style: { marginLeft: 'auto' } }
// break
// case 'center':
// attr = { ...attr, style: { marginLeft: 'auto', marginRight: 'auto' } }
// break
// default:
// attr = { ...attr, style: { textAlign: element.textAlign } }
// break
// }
// } else if (element.type === 'li') {
// switch (element.textAlign) {
// case 'right':
// attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'right' } }
// break
// case 'center':
// attr = { ...attr, style: { listStylePosition: 'inside', textAlign: 'center' } }
// break
// case 'left':
// default:
// attr = { ...attr, style: { listStylePosition: 'outside', textAlign: 'left' } }
// break
// }
// } else {
// attr = { ...attr, style: { textAlign: element.textAlign } }
// }
// }
// if (Element) {
// const el = (
// <Element
// attributes={attr}
// editorRef={editorRef}
// element={element}
// fieldProps={props}
// path={path}
// >
// {children}
// </Element>
// )
// return el
// }
return <div {...attr}>{children}</div>
}, [])
const renderLeaf = useCallback( const renderLeaf = useCallback(
({ attributes, children, leaf }) => { ({ attributes, children, leaf }) => {
const matchedLeaves = Object.entries(enabledLeaves).filter(([leafName]) => leaf[leafName]) const matchedLeaves = Object.entries(leaves).filter(([leafName]) => leaf[leafName])
console.log(matchedLeaves, leaf)
if (matchedLeaves.length > 0) { if (matchedLeaves.length > 0) {
return matchedLeaves.reduce( return matchedLeaves.reduce(
(result, [leafName], i) => { (result, [, leafConfig], i) => {
if (enabledLeaves[leafName]?.Leaf) { if (leafConfig?.Leaf) {
const Leaf = enabledLeaves[leafName]?.Leaf const Leaf = leafConfig.Leaf
return ( return (
<Leaf editorRef={editorRef} fieldProps={props} key={i} leaf={leaf} path={path}> <LeafProvider
{result} attributes={attributes}
</Leaf> editorRef={editorRef}
fieldProps={props}
key={i}
leaf={leaf}
path={path}
result={result}
schemaPath={schemaPath}
>
{Leaf}
</LeafProvider>
) )
} }
@@ -192,31 +231,15 @@ const RichText: React.FC<FieldProps> = (props) => {
return <span {...attributes}>{children}</span> return <span {...attributes}>{children}</span>
}, },
[enabledLeaves, path, props], [path, props, schemaPath, richTextComponentMap],
) )
const memoizedValidate = useCallback(
(value, validationOptions) => {
return validate(value, { ...validationOptions, required })
},
[validate, required],
)
const fieldType = useField({
condition,
path,
validate: memoizedValidate,
})
const { errorMessage, initialValue, setValue, showError, value } = fieldType
const classes = [ const classes = [
baseClass, baseClass,
'field-type', 'field-type',
className, className,
showError && 'error', showError && 'error',
readOnly && `${baseClass}--read-only`, readOnly && `${baseClass}--read-only`,
!hideGutter && `${baseClass}--gutter`,
] ]
.filter(Boolean) .filter(Boolean)
.join(' ') .join(' ')
@@ -225,12 +248,12 @@ const RichText: React.FC<FieldProps> = (props) => {
let CreatedEditor = withEnterBreakOut(withHistory(withReact(createEditor()))) let CreatedEditor = withEnterBreakOut(withHistory(withReact(createEditor())))
CreatedEditor = withHTML(CreatedEditor) CreatedEditor = withHTML(CreatedEditor)
CreatedEditor = enablePlugins(CreatedEditor, elements) // CreatedEditor = enablePlugins(CreatedEditor, elements)
CreatedEditor = enablePlugins(CreatedEditor, leaves) // CreatedEditor = enablePlugins(CreatedEditor, leaves)
return CreatedEditor return CreatedEditor
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [elements, leaves, path]) }, [path])
// All slate changes fire the onChange event // All slate changes fire the onChange event
// including selection changes // including selection changes
@@ -254,18 +277,6 @@ const RichText: React.FC<FieldProps> = (props) => {
[editor.operations, readOnly, setValue, value], [editor.operations, readOnly, setValue, value],
) )
useEffect(() => {
if (!loaded) {
const mergedElements = mergeCustomFunctions(elements, elementTypes)
const mergedLeaves = mergeCustomFunctions(leaves, leafTypes)
setEnabledElements(mergedElements)
setEnabledLeaves(mergedLeaves)
setLoaded(true)
}
}, [loaded, elements, leaves])
useEffect(() => { useEffect(() => {
function setClickableState(clickState: 'disabled' | 'enabled') { function setClickableState(clickState: 'disabled' | 'enabled') {
const selectors = 'button, a, [role="button"]' const selectors = 'button, a, [role="button"]'
@@ -280,16 +291,16 @@ const RichText: React.FC<FieldProps> = (props) => {
}) })
} }
if (loaded && readOnly) { if (readOnly) {
setClickableState('disabled') setClickableState('disabled')
} }
return () => { return () => {
if (loaded && readOnly) { if (readOnly) {
setClickableState('enabled') setClickableState('enabled')
} }
} }
}, [loaded, readOnly]) }, [readOnly])
// useEffect(() => { // useEffect(() => {
// // If there is a change to the initial value, we need to reset Slate history // // If there is a change to the initial value, we need to reset Slate history
@@ -301,10 +312,6 @@ const RichText: React.FC<FieldProps> = (props) => {
// } // }
// }, [path, editor]); // }, [path, editor]);
if (!loaded) {
return null
}
let valueToRender = value let valueToRender = value
if (typeof valueToRender === 'string') { if (typeof valueToRender === 'string') {
@@ -316,7 +323,7 @@ const RichText: React.FC<FieldProps> = (props) => {
} }
} }
if (!valueToRender) valueToRender = defaultValueFromProps || defaultRichTextValue if (!valueToRender) valueToRender = defaultRichTextValue
return ( return (
<div <div
@@ -327,8 +334,8 @@ const RichText: React.FC<FieldProps> = (props) => {
}} }}
> >
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<Error message={errorMessage} showError={showError} /> {Error}
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} /> {Label}
<Slate <Slate
editor={editor} editor={editor}
key={JSON.stringify({ initialValue, path })} key={JSON.stringify({ initialValue, path })}
@@ -336,7 +343,7 @@ const RichText: React.FC<FieldProps> = (props) => {
value={valueToRender as any[]} value={valueToRender as any[]}
> >
<div className={`${baseClass}__wrapper`}> <div className={`${baseClass}__wrapper`}>
{elements?.length + leaves?.length > 0 && ( {elements?.length + Object.keys(leaves)?.length > 0 && (
<div <div
className={[`${baseClass}__toolbar`, drawerIsOpen && `${baseClass}__drawerIsOpen`] className={[`${baseClass}__toolbar`, drawerIsOpen && `${baseClass}__drawerIsOpen`]
.filter(Boolean) .filter(Boolean)
@@ -344,7 +351,7 @@ const RichText: React.FC<FieldProps> = (props) => {
ref={toolbarRef} ref={toolbarRef}
> >
<div className={`${baseClass}__toolbar-wrap`}> <div className={`${baseClass}__toolbar-wrap`}>
{elements.map((element, i) => { {/* {elements.map((element, i) => {
let elementName: string let elementName: string
if (typeof element === 'object' && element?.name) elementName = element.name if (typeof element === 'object' && element?.name) elementName = element.name
if (typeof element === 'string') elementName = element if (typeof element === 'string') elementName = element
@@ -357,17 +364,21 @@ const RichText: React.FC<FieldProps> = (props) => {
} }
return null return null
})} })} */}
{leaves.map((leaf, i) => { {Object.values(leaves).map((leaf, i) => {
let leafName: string const Button = leaf?.Button
if (typeof leaf === 'object' && leaf?.name) leafName = leaf.name
if (typeof leaf === 'string') leafName = leaf
const leafType = enabledLeaves[leafName]
const Button = leafType?.Button
if (Button) { if (Button) {
return <Button fieldProps={props} key={i} path={path} /> return (
<LeafButtonProvider
fieldProps={props}
key={i}
path={path}
schemaPath={schemaPath}
>
{Button}
</LeafButtonProvider>
)
} }
return null return null
@@ -450,9 +461,10 @@ const RichText: React.FC<FieldProps> = (props) => {
</div> </div>
</div> </div>
</Slate> </Slate>
<FieldDescription description={description} path={path} value={value} /> {Description}
</div> </div>
</div> </div>
) )
} }
export default withCondition(RichText) export default withCondition(RichText)

View File

@@ -1,11 +1,18 @@
import React from 'react' import React from 'react'
import type { RichTextCustomLeaf } from '../../..'
import BoldIcon from '../../icons/Bold' import BoldIcon from '../../icons/Bold'
import { useLeaf } from '../../providers/LeafProvider'
import LeafButton from '../Button' import LeafButton from '../Button'
const Bold = ({ attributes, children }) => <strong {...attributes}>{children}</strong> const Bold = () => {
const { attributes, children } = useLeaf()
return <strong {...attributes}>{children}</strong>
}
const bold = { const bold: RichTextCustomLeaf = {
name: 'bold',
Button: () => ( Button: () => (
<LeafButton format="bold"> <LeafButton format="bold">
<BoldIcon /> <BoldIcon />

View File

@@ -1,11 +1,18 @@
import React from 'react' import React from 'react'
import type { RichTextCustomLeaf } from '../../..'
import CodeIcon from '../../icons/Code' import CodeIcon from '../../icons/Code'
import { useLeaf } from '../../providers/LeafProvider'
import LeafButton from '../Button' import LeafButton from '../Button'
const Code = ({ attributes, children }) => <code {...attributes}>{children}</code> const Code = () => {
const { attributes, children } = useLeaf()
return <code {...attributes}>{children}</code>
}
const code = { const code: RichTextCustomLeaf = {
name: 'code',
Button: () => ( Button: () => (
<LeafButton format="code"> <LeafButton format="code">
<CodeIcon /> <CodeIcon />

View File

@@ -1,13 +1,17 @@
import type { RichTextCustomLeaf } from '../..'
import bold from './bold' import bold from './bold'
import code from './code' import code from './code'
import italic from './italic' import italic from './italic'
import strikethrough from './strikethrough' import strikethrough from './strikethrough'
import underline from './underline' import underline from './underline'
export default { const defaultLeaves: Record<string, RichTextCustomLeaf> = {
bold, bold,
code, code,
italic, italic,
strikethrough, strikethrough,
underline, underline,
} }
export default defaultLeaves

View File

@@ -1,11 +1,18 @@
import React from 'react' import React from 'react'
import type { RichTextCustomLeaf } from '../../..'
import ItalicIcon from '../../icons/Italic' import ItalicIcon from '../../icons/Italic'
import { useLeaf } from '../../providers/LeafProvider'
import LeafButton from '../Button' import LeafButton from '../Button'
const Italic = ({ attributes, children }) => <em {...attributes}>{children}</em> const Italic = () => {
const { attributes, children } = useLeaf()
return <em {...attributes}>{children}</em>
}
const italic = { const italic: RichTextCustomLeaf = {
name: 'italic',
Button: () => ( Button: () => (
<LeafButton format="italic"> <LeafButton format="italic">
<ItalicIcon /> <ItalicIcon />

View File

@@ -1,11 +1,19 @@
'use client'
import React from 'react' import React from 'react'
import type { RichTextCustomLeaf } from '../../..'
import StrikethroughIcon from '../../icons/Strikethrough' import StrikethroughIcon from '../../icons/Strikethrough'
import { useLeaf } from '../../providers/LeafProvider'
import LeafButton from '../Button' import LeafButton from '../Button'
const Strikethrough = ({ attributes, children }) => <del {...attributes}>{children}</del> const Strikethrough = () => {
const { attributes, children } = useLeaf()
return <del {...attributes}>{children}</del>
}
const strikethrough = { const strikethrough: RichTextCustomLeaf = {
name: 'strikethrough',
Button: () => ( Button: () => (
<LeafButton format="strikethrough"> <LeafButton format="strikethrough">
<StrikethroughIcon /> <StrikethroughIcon />

View File

@@ -1,11 +1,18 @@
import React from 'react' import React from 'react'
import type { RichTextCustomLeaf } from '../../..'
import UnderlineIcon from '../../icons/Underline' import UnderlineIcon from '../../icons/Underline'
import { useLeaf } from '../../providers/LeafProvider'
import LeafButton from '../Button' import LeafButton from '../Button'
const Underline = ({ attributes, children }) => <u {...attributes}>{children}</u> const Underline = () => {
const { attributes, children } = useLeaf()
return <u {...attributes}>{children}</u>
}
const underline = { const underline: RichTextCustomLeaf = {
name: 'underline',
Button: () => ( Button: () => (
<LeafButton format="underline"> <LeafButton format="underline">
<UnderlineIcon /> <UnderlineIcon />

View File

@@ -0,0 +1,35 @@
'use client'
import React from 'react'
type ElementContextType = {
path: string
schemaPath: string
}
const ElementContext = React.createContext<ElementContextType>({
path: '',
schemaPath: '',
})
export const ElementProvider: React.FC<{
children: React.ReactNode
path: string
schemaPath: string
}> = (props) => {
const { children, ...rest } = props
return (
<ElementContext.Provider
value={{
...rest,
}}
>
{children}
</ElementContext.Provider>
)
}
export const useElement = () => {
const path = React.useContext(ElementContext)
return path
}

View File

@@ -0,0 +1,41 @@
'use client'
import React from 'react'
import type { FormFieldBase } from '../../../../ui/src/forms/fields/shared'
type LeafButtonContextType = {
fieldProps: FormFieldBase & {
name: string
}
path: string
schemaPath: string
}
const LeafButtonContext = React.createContext<LeafButtonContextType>({
fieldProps: {} as any,
path: '',
schemaPath: '',
})
export const LeafButtonProvider: React.FC<
LeafButtonContextType & {
children: React.ReactNode
}
> = (props) => {
const { children, ...rest } = props
return (
<LeafButtonContext.Provider
value={{
...rest,
}}
>
{children}
</LeafButtonContext.Provider>
)
}
export const useLeafButton = () => {
const path = React.useContext(LeafButtonContext)
return path
}

View File

@@ -0,0 +1,50 @@
'use client'
import React from 'react'
import type { FormFieldBase } from '../../../../ui/src/forms/fields/shared'
type LeafContextType = {
attributes: Record<string, unknown>
children: React.ReactNode
editorRef: React.MutableRefObject<HTMLDivElement>
fieldProps: FormFieldBase & {
name: string
}
leaf: string
path: string
schemaPath: string
}
const LeafContext = React.createContext<LeafContextType>({
attributes: {},
children: null,
editorRef: null,
fieldProps: {} as any,
leaf: '',
path: '',
schemaPath: '',
})
export const LeafProvider: React.FC<
LeafContextType & {
result: React.ReactNode
}
> = (props) => {
const { children, result, ...rest } = props
return (
<LeafContext.Provider
value={{
...rest,
children: result,
}}
>
{children}
</LeafContext.Provider>
)
}
export const useLeaf = () => {
const path = React.useContext(LeafContext)
return path
}

View File

@@ -2,13 +2,15 @@ import type { RichTextAdapter } from 'payload/types'
import { withMergedProps } from '@payloadcms/ui/utilities' import { withMergedProps } from '@payloadcms/ui/utilities'
import { withNullableJSONSchemaType } from 'payload/utilities' import { withNullableJSONSchemaType } from 'payload/utilities'
import React from 'react'
import type { AdapterArguments } from './types' import type { AdapterArguments, RichTextCustomLeaf } from './types'
import RichTextCell from './cell' import RichTextCell from './cell'
import { richTextRelationshipPromise } from './data/richTextRelationshipPromise' import { richTextRelationshipPromise } from './data/richTextRelationshipPromise'
import { richTextValidate } from './data/validation' import { richTextValidate } from './data/validation'
import RichTextField from './field' import RichTextField from './field'
import leafTypes from './field/leaves'
export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> { export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], AdapterArguments, any> {
return { return {
@@ -20,6 +22,29 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
Component: RichTextField, Component: RichTextField,
toMergeIntoProps: args, toMergeIntoProps: args,
}), }),
generateComponentMap: () => {
const componentMap = new Map()
;(args?.admin?.leaves || Object.values(leafTypes)).forEach((leaf) => {
let leafObject: RichTextCustomLeaf
if (typeof leaf === 'object' && leaf !== null) {
leafObject = leaf
} else if (typeof leaf === 'string' && leafTypes[leaf]) {
leafObject = leafTypes[leaf]
}
if (leafObject) {
const LeafButton = leafObject.Button
const LeafComponent = leafObject.Leaf
componentMap.set(`leaf.button.${leafObject.name}`, <LeafButton />)
componentMap.set(`leaf.component.${leafObject.name}`, <LeafComponent />)
}
})
return componentMap
},
outputSchema: ({ isRequired }) => { outputSchema: ({ isRequired }) => {
return { return {
items: { items: {

View File

@@ -10,6 +10,7 @@ export const RenderField: React.FC<{
const { path: pathFromContext, schemaPath: schemaPathFromContext } = useFieldPath() const { path: pathFromContext, schemaPath: schemaPathFromContext } = useFieldPath()
const path = `${pathFromContext ? `${pathFromContext}.` : ''}${name || ''}` const path = `${pathFromContext ? `${pathFromContext}.` : ''}${name || ''}`
const schemaPath = `${schemaPathFromContext ? `${schemaPathFromContext}.` : ''}${name || ''}` const schemaPath = `${schemaPathFromContext ? `${schemaPathFromContext}.` : ''}${name || ''}`
return ( return (
<FieldPathProvider path={path} schemaPath={schemaPath}> <FieldPathProvider path={path} schemaPath={schemaPath}>
{Field} {Field}

View File

@@ -1,35 +1,37 @@
'use client'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import type { RichTextAdapter, RichTextField } from 'payload/types' import type { RichTextAdapter } from 'payload/types'
import { Props } from './types'
const RichText: React.FC<RichTextField> = (fieldprops) => { const RichText: React.FC<Props> = (props) => {
// eslint-disable-next-line react/destructuring-assignment console.log(props)
const editor: RichTextAdapter = fieldprops.editor return null
// // eslint-disable-next-line react/destructuring-assignment
// const editor: RichTextAdapter = fieldprops.editor
const isLazy = 'LazyFieldComponent' in editor // const isLazy = 'LazyFieldComponent' in editor
const ImportedFieldComponent: React.FC<any> = useMemo(() => { // const ImportedFieldComponent: React.FC<any> = useMemo(() => {
return isLazy // return isLazy
? React.lazy(() => { // ? React.lazy(() => {
return editor.LazyFieldComponent().then((resolvedComponent) => ({ // return editor.LazyFieldComponent().then((resolvedComponent) => ({
default: resolvedComponent, // default: resolvedComponent,
})) // }))
}) // })
: null // : null
}, [editor, isLazy]) // }, [editor, isLazy])
if (isLazy) { // if (isLazy) {
return ( // return (
ImportedFieldComponent && ( // ImportedFieldComponent && (
<React.Suspense> // <React.Suspense>
<ImportedFieldComponent {...fieldprops} /> // <ImportedFieldComponent {...fieldprops} />
</React.Suspense> // </React.Suspense>
) // )
) // )
} // }
return <editor.FieldComponent {...fieldprops} /> // return <editor.FieldComponent {...fieldprops} />
} }
export default RichText export default RichText

View File

@@ -0,0 +1,3 @@
import { FormFieldBase } from '../shared'
export type Props = FormFieldBase

View File

@@ -8,6 +8,7 @@ import {
DocumentPreferences, DocumentPreferences,
JSONField, JSONField,
RelationshipField, RelationshipField,
RichTextField,
RowLabel, RowLabel,
UploadField, UploadField,
Validate, Validate,
@@ -108,6 +109,10 @@ export type FormFieldBase = {
// For `relationship` fields // For `relationship` fields
relationTo?: RelationshipField['relationTo'] relationTo?: RelationshipField['relationTo']
} }
| {
// For `richText` fields
richTextComponentMap?: Map<string, React.ReactNode>
}
) )
/** /**

View File

@@ -14,6 +14,7 @@ import DefaultLabel from '../../forms/Label'
import DefaultError from '../../forms/Error' import DefaultError from '../../forms/Error'
import DefaultDescription from '../../forms/FieldDescription' import DefaultDescription from '../../forms/FieldDescription'
import { HiddenInput } from '../..' import { HiddenInput } from '../..'
import { richText } from 'payload/fields/validations'
export const mapFields = (args: { export const mapFields = (args: {
fieldSchema: FieldWithPath[] fieldSchema: FieldWithPath[]
@@ -224,9 +225,10 @@ export const mapFields = (args: {
tabs, tabs,
blocks, blocks,
relationTo: 'relationTo' in field ? field.relationTo : undefined, relationTo: 'relationTo' in field ? field.relationTo : undefined,
richTextComponentMap: undefined,
} }
const Field = <FieldComponent {...fieldComponentProps} /> let Field = <FieldComponent {...fieldComponentProps} />
const cellComponentProps: CellProps = { const cellComponentProps: CellProps = {
fieldType: field.type, fieldType: field.type,
@@ -247,6 +249,34 @@ export const mapFields = (args: {
options: 'options' in field ? field.options : undefined, options: 'options' in field ? field.options : undefined,
} }
if (field.type === 'richText' && 'editor' in field) {
let RichTextComponent
const isLazy = 'LazyFieldComponent' in field.editor
if (isLazy) {
RichTextComponent = React.lazy(() => {
return 'LazyFieldComponent' in field.editor
? field.editor.LazyFieldComponent().then((resolvedComponent) => ({
default: resolvedComponent,
}))
: null
})
} else if ('FieldComponent' in field.editor) {
RichTextComponent = field.editor.FieldComponent
}
if (typeof field.editor.generateComponentMap === 'function') {
const result = field.editor.generateComponentMap()
// @ts-ignore-next-line // TODO: the `richTextComponentMap` is not found on the union type
fieldComponentProps.richTextComponentMap = result
}
if (RichTextComponent) {
Field = <RichTextComponent {...fieldComponentProps} />
}
}
const reducedField: MappedField = { const reducedField: MappedField = {
name: 'name' in field ? field.name : '', name: 'name' in field ? field.name : '',
label: 'label' in field && typeof field.label !== 'function' ? field.label : undefined, label: 'label' in field && typeof field.label !== 'function' ? field.label : undefined,
@@ -271,8 +301,8 @@ export const mapFields = (args: {
'label' in field && field.label && typeof field.label !== 'function' 'label' in field && field.label && typeof field.label !== 'function'
? field.label ? field.label
: 'name' in field : 'name' in field
? field.name ? field.name
: undefined : undefined
} }
name={'name' in field ? field.name : undefined} name={'name' in field ? field.name : undefined}
/> />

View File

@@ -0,0 +1,15 @@
import { RichTextField } from 'payload/types'
type elements = {
Button: React.ReactNode
}[]
export const mapRichText = (field: RichTextField) => {
let cellComponent = null
let leafComponent = null
let elements
if ('editor' in field) {
}
}

View File

@@ -7,6 +7,7 @@ import {
TabsField, TabsField,
Option, Option,
Labels, Labels,
RichTextField,
} from 'payload/types' } from 'payload/types'
import { fieldTypes } from '../../forms/fields' import { fieldTypes } from '../../forms/fields'
@@ -52,6 +53,10 @@ export type MappedField = {
*/ */
options?: Option[] options?: Option[]
hasMany?: boolean hasMany?: boolean
/**
* On `richText` fields only
*/
editor?: RichTextField['editor']
} }
export type FieldMap = MappedField[] export type FieldMap = MappedField[]

View File

@@ -14,6 +14,7 @@ export const getFormState = async (args: {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
credentials: 'include',
body: JSON.stringify(body), body: JSON.stringify(body),
}) })

View File

@@ -10,6 +10,10 @@ export const PostsCollection: CollectionConfig = {
name: 'text', name: 'text',
type: 'text', type: 'text',
}, },
{
name: 'richText',
type: 'richText',
},
{ {
name: 'associatedMedia', name: 'associatedMedia',
access: { access: {

View File

@@ -5,6 +5,7 @@ import type { Config, SanitizedConfig } from '../packages/payload/src/config/typ
import { mongooseAdapter } from '../packages/db-mongodb/src' import { mongooseAdapter } from '../packages/db-mongodb/src'
import { postgresAdapter } from '../packages/db-postgres/src' import { postgresAdapter } from '../packages/db-postgres/src'
import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build' import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build'
import { slateEditor } from '../packages/richtext-slate/src'
// process.env.PAYLOAD_DATABASE = 'postgres' // process.env.PAYLOAD_DATABASE = 'postgres'
@@ -24,8 +25,7 @@ const databaseAdapters = {
export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<SanitizedConfig> { export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<SanitizedConfig> {
const config: Config = { const config: Config = {
secret: 'TEST_SECRET', secret: 'TEST_SECRET',
// editor: slateEditor({}), editor: slateEditor({}),
editor: undefined,
rateLimit: { rateLimit: {
max: 9999999999, max: 9999999999,
window: 15 * 60 * 1000, // 15min default, window: 15 * 60 * 1000, // 15min default,