fix(richtext-lexical): Blocks: Nested Blocks having incorrect initial data (e.g. missing rows property) (#3638)
* fix(richtext-lexical): Blocks: Sub-Blocks having incorrect initial data (e.g. missing rows property) * chore: remove unnecessary comment
This commit is contained in:
@@ -8,6 +8,9 @@ import getSiblingData from './getSiblingData'
|
||||
import reduceFieldsToValues from './reduceFieldsToValues'
|
||||
import { flattenRows, separateRows } from './rows'
|
||||
|
||||
/**
|
||||
* Reducer which modifies the form field state (all the current data of the fields in the form). When called using dispatch, it will return a new state object.
|
||||
*/
|
||||
export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
switch (action.type) {
|
||||
case 'REPLACE_STATE': {
|
||||
|
||||
@@ -50,12 +50,15 @@ import reduceFieldsToValues from './reduceFieldsToValues'
|
||||
const baseClass = 'form'
|
||||
|
||||
const Form: React.FC<Props> = (props) => {
|
||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||
|
||||
const {
|
||||
action,
|
||||
children,
|
||||
className,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
fields: fieldsFromProps = collection?.fields || global?.fields,
|
||||
handleResponse,
|
||||
initialData, // values only, paths are required as key - form should build initial state as convenience
|
||||
initialState, // fully formed initial field state
|
||||
@@ -71,7 +74,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
const { code: locale } = useLocale()
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const { refreshCookie, user } = useAuth()
|
||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||
const operation = useOperation()
|
||||
|
||||
const config = useConfig()
|
||||
@@ -90,6 +92,10 @@ const Form: React.FC<Props> = (props) => {
|
||||
if (initialState) initialFieldState = initialState
|
||||
|
||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState)
|
||||
/**
|
||||
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
|
||||
* which calls the fieldReducer, which then updates the state.
|
||||
*/
|
||||
const [fields, dispatchFields] = fieldsReducer
|
||||
|
||||
contextRef.current.fields = fields
|
||||
@@ -169,7 +175,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
if (typeof field.validate === 'function') {
|
||||
let valueToValidate = field.value
|
||||
|
||||
if (Array.isArray(field.rows)) {
|
||||
if (field?.rows && Array.isArray(field.rows)) {
|
||||
valueToValidate = contextRef.current.getDataByPath(path)
|
||||
}
|
||||
|
||||
@@ -440,7 +446,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
const getRowSchemaByPath = React.useCallback(
|
||||
({ blockType, path }: { blockType?: string; path: string }) => {
|
||||
const rowConfig = traverseRowConfigs({
|
||||
fieldConfig: collection?.fields || global?.fields,
|
||||
fieldConfig: fieldsFromProps,
|
||||
path,
|
||||
})
|
||||
const rowFieldConfigs = buildFieldSchemaMap(rowConfig)
|
||||
@@ -448,10 +454,11 @@ const Form: React.FC<Props> = (props) => {
|
||||
const fieldKey = pathSegments.at(-1)
|
||||
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey)
|
||||
},
|
||||
[traverseRowConfigs, collection?.fields, global?.fields],
|
||||
[traverseRowConfigs, fieldsFromProps],
|
||||
)
|
||||
|
||||
// Array/Block row manipulation
|
||||
// Array/Block row manipulation. This is called when, for example, you add a new block to a blocks field.
|
||||
// The block data is saved in the rows property of the state, which is modified updated here.
|
||||
const addFieldRow: Context['addFieldRow'] = useCallback(
|
||||
async ({ data, path, rowIndex }) => {
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
@@ -2,7 +2,12 @@ import type React from 'react'
|
||||
import type { Dispatch } from 'react'
|
||||
|
||||
import type { User } from '../../../../auth/types'
|
||||
import type { Condition, Field as FieldConfig, Validate } from '../../../../fields/config/types'
|
||||
import type {
|
||||
Condition,
|
||||
Field,
|
||||
Field as FieldConfig,
|
||||
Validate,
|
||||
} from '../../../../fields/config/types'
|
||||
|
||||
export type Row = {
|
||||
blockType?: string
|
||||
@@ -41,6 +46,12 @@ export type Props = {
|
||||
className?: string
|
||||
disableSuccessStatus?: boolean
|
||||
disabled?: boolean
|
||||
/**
|
||||
* By default, the form will get the field schema (not data) from the current document. If you pass this in, you can override that behavior.
|
||||
* This is very useful for sub-forms, where the form's field schema is not necessarily the field schema of the current document (e.g. for the Blocks
|
||||
* feature of the Lexical Rich Text field)
|
||||
*/
|
||||
fields?: Field[]
|
||||
handleResponse?: (res: Response) => void
|
||||
initialData?: Data
|
||||
initialState?: Fields
|
||||
|
||||
@@ -17,10 +17,21 @@ const intersectionObserverOptions = {
|
||||
rootMargin: '1000px',
|
||||
}
|
||||
|
||||
// If you send `fields` through, it will render those fields explicitly
|
||||
// Otherwise, it will reduce your fields using the other provided props
|
||||
// This is so that we can conditionally render fields before reducing them, if desired
|
||||
// See the sidebar in '../collections/Edit/Default/index.tsx' for an example
|
||||
/**
|
||||
* If you send `fields` through, it will render those fields explicitly
|
||||
* Otherwise, it will reduce your fields using the other provided props
|
||||
* This is so that we can conditionally render fields before reducing them, if desired
|
||||
* See the sidebar in '../collections/Edit/Default/index.tsx' for an example
|
||||
*
|
||||
* The state/data for the fields it renders is not managed by this component. Instead, every component it renders has
|
||||
* their own handling of their own value, usually through the useField hook. This hook will get the field's value
|
||||
* from the Form the field is in, using the field's path.
|
||||
*
|
||||
* Thus, if you would like to set the value of a field you render here, you must do so in the Form that contains the field, or in the
|
||||
* Field component itself.
|
||||
*
|
||||
* All this component does is render the field's Field Components, and pass them the props they need to function.
|
||||
**/
|
||||
const RenderFields: React.FC<Props> = (props) => {
|
||||
const { className, fieldTypes, forceRender, margins } = props
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
|
||||
let valueToValidate = value
|
||||
|
||||
if (Array.isArray(field.rows)) {
|
||||
if (field?.rows && Array.isArray(field.rows)) {
|
||||
valueToValidate = getDataByPath(path)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
}
|
||||
}
|
||||
|
||||
validateField()
|
||||
void validateField()
|
||||
},
|
||||
150,
|
||||
[
|
||||
|
||||
@@ -24,6 +24,11 @@ type Props = {
|
||||
nodeKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual content of the Block. This should be INSIDE a Form component,
|
||||
* scoped to the block. All format operations in here are thus scoped to the block's form, and
|
||||
* not the whole document.
|
||||
*/
|
||||
export const BlockContent: React.FC<Props> = (props) => {
|
||||
const { baseClass, block, field, fields, nodeKey } = props
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { type ElementFormatType } from 'lexical'
|
||||
import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
|
||||
import { type BlockFields } from '../nodes/BlocksNode'
|
||||
const baseClass = 'lexical-block'
|
||||
|
||||
import type { Data } from 'payload/types'
|
||||
|
||||
import { useConfig } from 'payload/components/utilities'
|
||||
import {
|
||||
buildStateFromSchema,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
} from 'payload/components/utilities'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { BlocksFeatureProps } from '..'
|
||||
|
||||
@@ -43,13 +49,49 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const initialDataRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
const initialStateRef = React.useRef<Data>(buildInitialState(fields.data || {})) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||
|
||||
const config = useConfig()
|
||||
const { t } = useTranslation('general')
|
||||
const { code: locale } = useLocale()
|
||||
const { getDocPreferences } = useDocumentInfo()
|
||||
|
||||
// initialState State
|
||||
|
||||
const [initialState, setInitialState] = React.useState<Data>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function buildInitialState() {
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
const stateFromSchema = await buildStateFromSchema({
|
||||
config,
|
||||
data: fields.data,
|
||||
fieldSchema: block.fields,
|
||||
locale,
|
||||
operation: 'update',
|
||||
preferences,
|
||||
t,
|
||||
})
|
||||
|
||||
// We have to merge the output of buildInitialState (above this useEffect) with the output of buildStateFromSchema.
|
||||
// That's because the output of buildInitialState provides important properties necessary for THIS block,
|
||||
// like blockName, blockType and id, while buildStateFromSchema provides the correct output of this block's data,
|
||||
// e.g. if this block has a sub-block (like the `rows` property)
|
||||
setInitialState({
|
||||
...initialStateRef?.current,
|
||||
...stateFromSchema,
|
||||
})
|
||||
}
|
||||
void buildInitialState()
|
||||
}, [setInitialState, config, block, locale, getDocPreferences, t]) // do not add fields here, it causes an endless loop
|
||||
|
||||
// Memoized Form JSX
|
||||
const formContent = useMemo(() => {
|
||||
return (
|
||||
block && (
|
||||
<Form initialState={initialDataRef?.current} submitted={submitted}>
|
||||
block &&
|
||||
initialState && (
|
||||
<Form fields={block.fields} initialState={initialState} submitted={submitted}>
|
||||
<BlockContent
|
||||
baseClass={baseClass}
|
||||
block={block}
|
||||
@@ -60,7 +102,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
</Form>
|
||||
)
|
||||
)
|
||||
}, [block, field, nodeKey, submitted])
|
||||
}, [block, field, nodeKey, submitted, initialState])
|
||||
|
||||
return <div className={baseClass}>{formContent}</div>
|
||||
}
|
||||
|
||||
108
test/fields/collections/Lexical/blocks.ts
Normal file
108
test/fields/collections/Lexical/blocks.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { Block } from '../../../../packages/payload/src/fields/config/types'
|
||||
|
||||
import { lexicalEditor } from '../../../../packages/richtext-lexical/src'
|
||||
|
||||
export const TextBlock: Block = {
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
slug: 'text',
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
export const SubBlockBlock: Block = {
|
||||
slug: 'subBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'subBlocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'contentBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'textArea',
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
320
test/fields/collections/Lexical/generateLexicalRichText.ts
Normal file
320
test/fields/collections/Lexical/generateLexicalRichText.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { loremIpsum } from './loremIpsum'
|
||||
|
||||
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,
|
||||
type: 'link',
|
||||
version: 1,
|
||||
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,
|
||||
type: 'link',
|
||||
version: 1,
|
||||
fields: {
|
||||
url: 'https://',
|
||||
doc: {
|
||||
value: {
|
||||
id: '{{ARRAY_DOC_ID}}',
|
||||
},
|
||||
relationTo: 'array-fields',
|
||||
},
|
||||
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: 1,
|
||||
value: {
|
||||
id: '{{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: 1,
|
||||
relationTo: 'uploads',
|
||||
value: {
|
||||
id: '{{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',
|
||||
},
|
||||
}
|
||||
}
|
||||
87
test/fields/collections/Lexical/index.ts
Normal file
87
test/fields/collections/Lexical/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
LinkFeature,
|
||||
TreeviewFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '../../../../packages/richtext-lexical/src'
|
||||
import {
|
||||
RelationshipBlock,
|
||||
SelectFieldBlock,
|
||||
SubBlockBlock,
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
} from './blocks'
|
||||
import { generateLexicalRichText } from './generateLexicalRichText'
|
||||
|
||||
export const LexicalFields: CollectionConfig = {
|
||||
slug: 'lexical-fields',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'richTextLexicalCustomFields',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeviewFeature(),
|
||||
LinkFeature({
|
||||
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.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
uploads: {
|
||||
fields: [
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
TextBlock,
|
||||
UploadAndRichTextBlock,
|
||||
SelectFieldBlock,
|
||||
RelationshipBlock,
|
||||
SubBlockBlock,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const lexicalRichTextDoc = {
|
||||
title: 'Rich Text',
|
||||
richTextLexicalCustomFields: generateLexicalRichText(),
|
||||
}
|
||||
2
test/fields/collections/Lexical/loremIpsum.ts
Normal file
2
test/fields/collections/Lexical/loremIpsum.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
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.'
|
||||
@@ -14,6 +14,7 @@ import DateFields, { dateDoc } from './collections/Date'
|
||||
import GroupFields, { groupDoc } from './collections/Group'
|
||||
import IndexedFields from './collections/Indexed'
|
||||
import JSONFields, { jsonDoc } from './collections/JSON'
|
||||
import { LexicalFields } from './collections/Lexical'
|
||||
import NumberFields, { numberDoc } from './collections/Number'
|
||||
import PointFields, { pointDoc } from './collections/Point'
|
||||
import RadioFields, { radiosDoc } from './collections/Radio'
|
||||
@@ -41,6 +42,7 @@ export default buildConfigWithDefaults({
|
||||
}),
|
||||
},
|
||||
collections: [
|
||||
LexicalFields,
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
@@ -147,6 +149,14 @@ export default buildConfigWithDefaults({
|
||||
.replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID),
|
||||
)
|
||||
|
||||
const lexicalRichTextDocWithRelId = JSON.parse(
|
||||
JSON.stringify(richTextDoc)
|
||||
.replace(/"\{\{ARRAY_DOC_ID\}\}"/g, formattedID)
|
||||
.replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, formattedJPGID)
|
||||
.replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID),
|
||||
)
|
||||
await payload.create({ collection: 'lexical-fields', data: lexicalRichTextDocWithRelId })
|
||||
|
||||
const richTextDocWithRelationship = { ...richTextDocWithRelId }
|
||||
|
||||
await payload.create({ collection: 'rich-text-fields', data: richTextBulletsDocWithRelId })
|
||||
|
||||
Reference in New Issue
Block a user