Merge pull request #3449 from payloadcms/feat/2.0-lexical-block-validations

BREAKING: config (SanitizedConfig) is now a new, mandatory property to be passed into .validate(, options) functions. In order to accommodate that, other functions which may call validate now also have a new, mandatory config property. These are:
* buildStateFromSchema
* addFieldStatePromise


feat: breaking: richtext-lexical: block node validations
This commit is contained in:
Alessio Gravili
2023-10-06 16:56:59 +02:00
committed by GitHub
50 changed files with 582 additions and 142 deletions

View File

@@ -65,7 +65,7 @@
"rimraf": "3.0.2",
"shelljs": "0.8.5",
"ts-node": "10.9.1",
"turbo": "^1.10.13",
"turbo": "^1.10.15",
"typescript": "5.2.2",
"uuid": "^9.0.0"
},

View File

@@ -41,6 +41,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
const hasInitializedState = useRef(false)
const [isOpen, setIsOpen] = useState(false)
const [collectionConfig] = useRelatedCollections(collectionSlug)
const config = useConfig()
const { admin: { components: { views: { Edit } = {} } = {} } = {} } = collectionConfig
@@ -82,6 +83,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
id,
config,
data,
fieldSchema: fields,
locale,

View File

@@ -4,6 +4,7 @@ import type { TFunction } from 'i18next'
import ObjectID from 'bson-objectid'
import type { User } from '../../../../../auth'
import type { SanitizedConfig } from '../../../../../config/types'
import type { NonPresentationalField } from '../../../../../fields/config/types'
import type { Data, Fields, FormField } from '../types'
@@ -12,6 +13,7 @@ import getValueWithDefault from '../../../../../fields/getDefaultValue'
import { iterateFields } from './iterateFields'
type Args = {
config: SanitizedConfig
data: Data
field: NonPresentationalField
fullData: Data
@@ -30,6 +32,7 @@ type Args = {
export const addFieldStatePromise = async ({
id,
config,
data,
field,
fullData,
@@ -68,6 +71,7 @@ export const addFieldStatePromise = async ({
validationResult = await fieldState.validate(data?.[field.name], {
...field,
id,
config,
data: fullData,
operation,
siblingData: data,
@@ -100,6 +104,7 @@ export const addFieldStatePromise = async ({
acc.promises.push(
iterateFields({
id,
config,
data: row,
fields: field.fields,
fullData,
@@ -188,6 +193,7 @@ export const addFieldStatePromise = async ({
acc.promises.push(
iterateFields({
id,
config,
data: row,
fields: block.fields,
fullData,
@@ -249,6 +255,7 @@ export const addFieldStatePromise = async ({
case 'group': {
await iterateFields({
id,
config,
data: data?.[field.name] || {},
fields: field.fields,
fullData,
@@ -348,6 +355,7 @@ export const addFieldStatePromise = async ({
// Handle field types that do not use names (row, etc)
await iterateFields({
id,
config,
data,
fields: field.fields,
fullData,
@@ -364,6 +372,7 @@ export const addFieldStatePromise = async ({
const promises = field.tabs.map((tab) =>
iterateFields({
id,
config,
data: tabHasName(tab) ? data?.[tab.name] : data,
fields: tab.fields,
fullData,

View File

@@ -1,12 +1,14 @@
import type { TFunction } from 'i18next'
import type { User } from '../../../../../auth'
import type { SanitizedConfig } from '../../../../../config/types'
import type { Field as FieldSchema } from '../../../../../fields/config/types'
import type { Data, Fields } from '../types'
import { iterateFields } from './iterateFields'
type Args = {
config: SanitizedConfig
data?: Data
fieldSchema: FieldSchema[]
id?: number | string
@@ -21,13 +23,24 @@ type Args = {
}
const buildStateFromSchema = async (args: Args): Promise<Fields> => {
const { id, data: fullData = {}, fieldSchema, locale, operation, preferences, t, user } = args
const {
id,
config,
data: fullData = {},
fieldSchema,
locale,
operation,
preferences,
t,
user,
} = args
if (fieldSchema) {
const state: Fields = {}
await iterateFields({
id,
config,
data: fullData,
fields: fieldSchema,
fullData,

View File

@@ -1,6 +1,7 @@
import type { TFunction } from 'i18next'
import type { User } from '../../../../../auth'
import type { SanitizedConfig } from '../../../../../config/types'
import type { Field as FieldSchema } from '../../../../../fields/config/types'
import type { Data, Fields } from '../types'
@@ -8,6 +9,7 @@ import { fieldIsPresentationalOnly } from '../../../../../fields/config/types'
import { addFieldStatePromise } from './addFieldStatePromise'
type Args = {
config: SanitizedConfig
data: Data
fields: FieldSchema[]
fullData: Data
@@ -26,6 +28,7 @@ type Args = {
export const iterateFields = async ({
id,
config,
data,
fields,
fullData,
@@ -51,6 +54,7 @@ export const iterateFields = async ({
promises.push(
addFieldStatePromise({
id,
config,
data,
field,
fullData,

View File

@@ -24,6 +24,7 @@ import wait from '../../../../utilities/wait'
import { requests } from '../../../api'
import useThrottledEffect from '../../../hooks/useThrottledEffect'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
import { useOperation } from '../../utilities/OperationProvider'
@@ -62,6 +63,7 @@ const Form: React.FC<Props> = (props) => {
onSubmit,
onSuccess,
redirect,
submitted: submittedFromProps,
waitForAutocomplete,
} = props
@@ -72,6 +74,8 @@ const Form: React.FC<Props> = (props) => {
const { id, collection, getDocPreferences, global } = useDocumentInfo()
const operation = useOperation()
const config = useConfig()
const [modified, setModified] = useState(false)
const [processing, setProcessing] = useState(false)
const [submitted, setSubmitted] = useState(false)
@@ -165,6 +169,7 @@ const Form: React.FC<Props> = (props) => {
if (typeof field.validate === 'function') {
validationResult = await field.validate(field.value, {
id,
config,
data,
operation,
siblingData: contextRef.current.getSiblingData(path),
@@ -191,7 +196,7 @@ const Form: React.FC<Props> = (props) => {
}
return isValid
}, [contextRef, id, user, operation, t, dispatchFields])
}, [contextRef, id, user, operation, t, dispatchFields, config])
const submit = useCallback(
async (options: SubmitOptions = {}, e): Promise<void> => {
@@ -452,6 +457,7 @@ const Form: React.FC<Props> = (props) => {
if (fieldConfig) {
const subFieldState = await buildStateFromSchema({
id,
config,
data,
fieldSchema: fieldConfig,
locale,
@@ -469,7 +475,7 @@ const Form: React.FC<Props> = (props) => {
})
}
},
[dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath],
[dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath, config],
)
const removeFieldRow: Context['removeFieldRow'] = useCallback(
@@ -490,6 +496,7 @@ const Form: React.FC<Props> = (props) => {
if (fieldConfig) {
const subFieldState = await buildStateFromSchema({
id,
config,
data,
fieldSchema: fieldConfig,
locale,
@@ -507,7 +514,7 @@ const Form: React.FC<Props> = (props) => {
})
}
},
[dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath],
[dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath, config],
)
const getFields = useCallback(() => contextRef.current.fields, [contextRef])
@@ -557,6 +564,7 @@ const Form: React.FC<Props> = (props) => {
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
id,
config,
data,
fieldSchema,
locale,
@@ -569,7 +577,7 @@ const Form: React.FC<Props> = (props) => {
setModified(false)
dispatchFields({ state, type: 'REPLACE_STATE' })
},
[id, user, operation, locale, t, dispatchFields, getDocPreferences],
[id, user, operation, locale, t, dispatchFields, getDocPreferences, config],
)
const replaceState = useCallback(
@@ -601,6 +609,10 @@ const Form: React.FC<Props> = (props) => {
contextRef.current.removeFieldRow = removeFieldRow
contextRef.current.replaceFieldRow = replaceFieldRow
useEffect(() => {
if (typeof submittedFromProps === 'boolean') setSubmitted(submittedFromProps)
}, [submittedFromProps])
useEffect(() => {
if (initialState) {
contextRef.current = { ...initContextState } as FormContextType

View File

@@ -49,6 +49,7 @@ export type Props = {
onSubmit?: (fields: Fields, data: Data) => void
onSuccess?: (json: unknown) => void
redirect?: string
submitted?: boolean
validationOperation?: 'create' | 'update'
waitForAutocomplete?: boolean
}

View File

@@ -3,12 +3,9 @@ import React from 'react'
import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextAdapter } from './types'
import { useConfig } from '../../../utilities/Config'
const RichText: React.FC<RichTextField> = (props) => {
const config = useConfig()
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.editor || config.editor
const editor: RichTextAdapter = props.editor
return <editor.FieldComponent {...props} />
}

View File

@@ -1,5 +1,5 @@
import type { PayloadRequest } from '../../../../../express/types'
import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextField, Validate } from '../../../../../fields/config/types'
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
export type RichTextFieldProps<AdapterProps = unknown> = Omit<
@@ -21,4 +21,5 @@ export type RichTextAdapter<AdapterProps = unknown> = {
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
validate: Validate<unknown, unknown, RichTextField<AdapterProps>>
}

View File

@@ -6,6 +6,7 @@ import type { FieldType, Options } from './types'
import useThrottledEffect from '../../../hooks/useThrottledEffect'
import { useAuth } from '../../utilities/Auth'
import { useConfig } from '../../utilities/Config'
import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useOperation } from '../../utilities/OperationProvider'
import { useForm, useFormFields, useFormProcessing, useFormSubmitted } from '../Form/context'
@@ -26,6 +27,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
const field = useFormFields(([fields]) => fields[path])
const { t } = useTranslation()
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
const config = useConfig()
const { getData, getSiblingData, setModified } = useForm()
@@ -106,6 +108,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
const validateOptions = {
id,
config,
data: getData(),
operation,
siblingData: getSiblingData(path),

View File

@@ -25,6 +25,7 @@ const AccountView: React.FC = () => {
const { id, docPermissions, getDocPermissions, getDocPreferences, preferencesKey, slug } =
useDocumentInfo()
const { getPreference } = usePreferences()
const config = useConfig()
const {
admin: {
@@ -65,6 +66,7 @@ const AccountView: React.FC = () => {
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
id,
config,
data: json.doc,
fieldSchema: collection.fields,
locale,
@@ -94,6 +96,7 @@ const AccountView: React.FC = () => {
const state = await buildStateFromSchema({
id,
config,
data: dataToRender,
fieldSchema: fields,
locale,

View File

@@ -29,6 +29,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
useDocumentInfo()
const { getPreference } = usePreferences()
const { t } = useTranslation()
const config = useConfig()
const {
routes: { api },
@@ -44,6 +45,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
setUpdatedAt(json?.result?.updatedAt)
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data: json.result,
fieldSchema: fields,
locale,
@@ -68,6 +70,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const awaitInitialState = async () => {
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data: dataToRender,
fieldSchema: fields,
locale,

View File

@@ -31,10 +31,11 @@ const EditView: React.FC<IndexProps> = (props) => {
const { code: locale } = useLocale()
const config = useConfig()
const {
routes: { admin, api },
serverURL,
} = useConfig()
} = config
const { params: { id } = {} } = useRouteMatch<Record<string, string>>()
const history = useHistory()
@@ -56,6 +57,7 @@ const EditView: React.FC<IndexProps> = (props) => {
const state = await buildStateFromSchema({
id,
config,
data: doc || {},
fieldSchema: overrides.fieldSchema,
locale,

View File

@@ -4,12 +4,9 @@ import type { RichTextField } from '../../../../../../../../fields/config/types'
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
import type { CellComponentProps } from '../../types'
import { useConfig } from '../../../../../../utilities/Config'
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
const config = useConfig()
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.field.editor || config.editor
const editor: RichTextAdapter = props.field.editor
return <editor.CellComponent {...props} />
}

View File

@@ -86,6 +86,7 @@ export default joi.object({
CellComponent: component.required(),
FieldComponent: component.required(),
afterReadPromise: joi.func().required(),
validate: joi.func().required(),
}),
email: joi.object(),
endpoints: endpointsSchema,

View File

@@ -32,6 +32,11 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi
throw new InvalidFieldName(field, field.name)
}
// Make sure that the richText field has an editor
if (field.type === 'richText' && !field.editor && config.editor) {
field.editor = config.editor
}
// Auto-label
if (
'name' in field &&

View File

@@ -358,6 +358,7 @@ export const richText = baseField.keys({
CellComponent: componentSchema.required(),
FieldComponent: componentSchema.required(),
afterReadPromise: joi.func().required(),
validate: joi.func().required(),
}),
type: joi.string().valid('richText').required(),
})

View File

@@ -10,6 +10,7 @@ import type { RowLabel } from '../../admin/components/forms/RowLabel/types'
import type { RichTextAdapter } from '../../admin/components/forms/field-types/RichText/types'
import type { User } from '../../auth'
import type { TypeWithID } from '../../collections/config/types'
import type { SanitizedConfig } from '../../config/types'
import type { PayloadRequest, RequestContext } from '../../express/types'
import type { Payload } from '../../payload'
import type { Operation, Where } from '../../types'
@@ -91,6 +92,7 @@ export type Labels = {
}
export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
config: SanitizedConfig
data: Partial<TData>
id?: number | string
operation?: Operation
@@ -100,6 +102,7 @@ export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
user?: Partial<User>
} & TFieldConfig
// TODO: Having TFieldConfig as any breaks all type checking / auto-completions for the base ValidateOptions properties.
export type Validate<TValue = any, TData = any, TSiblingData = any, TFieldConfig = any> = (
value: TValue,
options: ValidateOptions<TData, TSiblingData, TFieldConfig>,

View File

@@ -128,7 +128,7 @@ export const promise = async ({
}
case 'richText': {
const editor: RichTextAdapter = field?.editor || req?.payload?.config?.editor
const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) {
const afterReadPromise = editor.afterReadPromise({
currentDepth,

View File

@@ -106,6 +106,7 @@ export const promise = async ({
const validationResult = await field.validate(valueToValidate, {
...field,
id,
config: req.payload.config,
data: merge(doc, data, { arrayMerge: (_, source) => source }),
jsonError,
operation,

View File

@@ -1,3 +1,4 @@
import type { RichTextAdapter } from '../exports/types'
import type {
ArrayField,
BlockField,
@@ -11,6 +12,7 @@ import type {
RadioField,
RelationshipField,
RelationshipValue,
RichTextField,
SelectField,
TextField,
TextareaField,
@@ -212,6 +214,15 @@ export const date: Validate<unknown, unknown, DateField> = (value, { required, t
return true
}
export const richText: Validate<unknown, unknown, RichTextField, RichTextField> = async (
value,
options,
) => {
const editor: RichTextAdapter = options?.editor
return await editor.validate(value, options)
}
const validateFilterOptions: Validate = async (
value,
{ id, data, filterOptions, payload, relationTo, siblingData, t, user },
@@ -511,6 +522,7 @@ export default {
point,
radio,
relationship,
richText,
select,
text,
textarea,

View File

@@ -427,7 +427,7 @@ function buildObjectType({
async resolve(parent, args, context) {
let depth = payload.config.defaultDepth
if (typeof args.depth !== 'undefined') depth = args.depth
const editor: RichTextAdapter = field?.editor || payload?.config?.editor
const editor: RichTextAdapter = field?.editor
if (editor?.afterReadPromise) {
await editor?.afterReadPromise({

View File

@@ -35,6 +35,7 @@
"@lexical/selection": "0.12.2",
"@lexical/table": "0.12.2",
"@lexical/utils": "0.12.2",
"bson-objectid": "2.0.4",
"classnames": "^2.3.2",
"i18next": "22.5.1",
"katex": "0.16.8",

View File

@@ -6,7 +6,8 @@ import { ErrorBoundary } from 'react-error-boundary'
import type { FieldProps } from '../types'
import { richTextValidate } from '../populate/validation'
import { defaultRichTextValueV2 } from '../populate/defaultValue'
import { richTextValidateHOC } from '../validate'
import './index.scss'
import { LexicalProvider } from './lexical/LexicalProvider'
@@ -23,22 +24,21 @@ const RichText: React.FC<FieldProps> = (props) => {
style,
width,
},
admin,
defaultValue: defaultValueFromProps,
editorConfig,
label,
path: pathFromProps,
required,
validate = richTextValidate,
validate = richTextValidateHOC({ editorConfig }),
} = props
const path = pathFromProps || name
const memoizedValidate = useCallback(
(value, validationOptions) => {
return validate(value, { ...validationOptions, required })
return validate(value, { ...validationOptions, props, required })
},
[validate, required],
[validate, required, props],
)
const fieldType = useField<SerializedEditorState>({
@@ -49,6 +49,19 @@ const RichText: React.FC<FieldProps> = (props) => {
const { errorMessage, initialValue, setValue, showError, value } = fieldType
let valueToUse = value
if (typeof valueToUse === 'string') {
try {
const parsedJSON = JSON.parse(valueToUse)
valueToUse = parsedJSON
} catch (err) {
valueToUse = null
}
}
if (!valueToUse) valueToUse = defaultValueFromProps || defaultRichTextValueV2
const classes = [
baseClass,
'field-type',

View File

@@ -1,4 +1,4 @@
import type { Block, Data } from 'payload/types'
import type { Block, Data, Fields } from 'payload/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $getNodeByKey } from 'lexical'
@@ -6,8 +6,9 @@ import { Button, ErrorPill, Pill } from 'payload/components'
import { Collapsible } from 'payload/components/elements'
import { SectionTitle } from 'payload/components/fields/Blocks'
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
import { useDocumentInfo } from 'payload/components/utilities'
import { getTranslation } from 'payload/utilities'
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import type { FieldProps } from '../../../../types'
@@ -27,10 +28,31 @@ export const BlockContent: React.FC<Props> = (props) => {
const { baseClass, block, field, fields, nodeKey } = props
const { i18n } = useTranslation()
const [editor] = useLexicalComposerContext()
const [collapsed, setCollapsed] = React.useState<boolean>(fields.collapsed)
// Used for saving collapsed to preferences (and gettin' it from there again)
// Remember, these preferences are scoped to the whole document, not just this form. This
// is important to consider for the data path used in setDocFieldPreferences
const { getDocPreferences, setDocFieldPreferences } = useDocumentInfo()
const [collapsed, setCollapsed] = React.useState<boolean>(() => {
let initialState = false
getDocPreferences().then((currentDocPreferences) => {
const currentFieldPreferences = currentDocPreferences?.fields[field.name]
const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed
if (collapsedMap && collapsedMap[fields.data.id] !== undefined) {
setCollapsed(collapsedMap[fields.data.id])
initialState = collapsedMap[fields.data.id]
}
})
return initialState
})
const hasSubmitted = useFormSubmitted()
const childErrorPathsCount = 0 // TODO row.childErrorPaths?.size
const fieldHasErrors = hasSubmitted && childErrorPathsCount > 0
const [errorCount, setErrorCount] = React.useState(0)
const fieldHasErrors = hasSubmitted && errorCount > 0
const classNames = [
`${baseClass}__row`,
@@ -42,31 +64,46 @@ export const BlockContent: React.FC<Props> = (props) => {
const path = '' as const
const onFormChange = useCallback(
({ formData }: { formData: Data }) => {
({ fields: formFields, formData }: { fields: Fields; formData: Data }) => {
editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey)
if (node) {
node.setFields({
collapsed: collapsed,
data: formData as any,
})
}
})
// update error count
if (hasSubmitted) {
let rowErrorCount = 0
for (const formField of Object.values(formFields)) {
if (formField?.valid === false) {
rowErrorCount++
}
}
setErrorCount(rowErrorCount)
}
},
[editor, nodeKey, collapsed],
[editor, nodeKey, hasSubmitted],
)
const onCollapsedChange = useCallback(() => {
editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey)
if (node) {
node.setFields({
...node.getFields(),
collapsed: collapsed,
getDocPreferences().then((currentDocPreferences) => {
const currentFieldPreferences = currentDocPreferences?.fields[field.name]
const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed
const newCollapsed: { [key: string]: boolean } =
collapsedMap && collapsedMap?.size ? collapsedMap : {}
newCollapsed[fields.data.id] = !collapsed
setDocFieldPreferences(field.name, {
collapsed: newCollapsed,
})
}
})
}, [editor, nodeKey, collapsed])
}, [collapsed, getDocPreferences, field.name, setDocFieldPreferences, fields.data.id])
const removeBlock = useCallback(() => {
editor.update(() => {
@@ -79,7 +116,7 @@ export const BlockContent: React.FC<Props> = (props) => {
<Collapsible
className={classNames}
collapsed={collapsed}
collapsibleStyle={false ? 'error' : 'default'}
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
header={
<div className={`${baseClass}__block-header`}>
<div>
@@ -90,7 +127,7 @@ export const BlockContent: React.FC<Props> = (props) => {
{getTranslation(block.labels.singular, i18n)}
</Pill>
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage />}
{fieldHasErrors && <ErrorPill count={errorCount} withMessage />}
</div>
<Button
buttonStyle="icon-label"

View File

@@ -1,4 +1,4 @@
import type { Data, FieldWithPath } from 'payload/types'
import type { Data, FieldWithPath, Fields } from 'payload/types'
import type React from 'react'
import { reduceFieldsToValues, useAllFormFields } from 'payload/components/forms'
@@ -8,7 +8,7 @@ import './index.scss'
type Props = {
fieldSchema: FieldWithPath[]
onChange?: ({ formData }: { formData: Data }) => void
onChange?: ({ fields, formData }: { fields: Fields; formData: Data }) => void
}
export const FormSavePlugin: React.FC<Props> = (props) => {
@@ -22,9 +22,9 @@ export const FormSavePlugin: React.FC<Props> = (props) => {
useEffect(() => {
if (onChange) {
onChange({ formData })
onChange({ fields, formData })
}
}, [formData, onChange])
}, [formData, onChange, fields])
return null
}

View File

@@ -1,5 +1,5 @@
import { type ElementFormatType } from 'lexical'
import { Form, buildInitialState } from 'payload/components/forms'
import { Form, buildInitialState, useFormSubmitted } from 'payload/components/forms'
import React, { useMemo } from 'react'
import { type BlockFields } from '../nodes/BlocksNode'
@@ -27,6 +27,7 @@ type Props = {
export const BlockComponent: React.FC<Props> = (props) => {
const { children, className, fields, format, nodeKey } = props
const payloadConfig = useConfig()
const submitted = useFormSubmitted()
const { editorConfig, field } = useEditorConfigContext()
@@ -48,7 +49,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
const formContent = useMemo(() => {
return (
block && (
<Form initialState={initialDataRef?.current}>
<Form initialState={initialDataRef?.current} submitted={submitted}>
<BlockContent
baseClass={baseClass}
block={block}
@@ -59,7 +60,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
</Form>
)
)
}, [block, field, nodeKey])
}, [block, field, nodeKey, submitted])
return <div className={baseClass}>{formContent}</div>
}

View File

@@ -36,7 +36,6 @@ const insertBlock = ({
}) => {
if (!replaceNodeKey) {
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
collapsed: false,
data: {
blockName: '',
blockType: blockType,
@@ -48,7 +47,6 @@ const insertBlock = ({
if (node) {
node.replace(
$createBlockNode({
collapsed: false,
data: {
blockName: '',
blockType: blockType,

View File

@@ -8,10 +8,10 @@ import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
import { BlockIcon } from '../../lexical/ui/icons/Block'
import { blockAfterReadPromiseHOC } from './afterReadPromise'
import { INSERT_BLOCK_WITH_DRAWER_COMMAND } from './drawer'
import './index.scss'
import { BlockNode } from './nodes/BlocksNode'
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
import { blockValidationHOC } from './validate'
export type BlocksFeatureProps = {
blocks: Block[]
@@ -45,6 +45,7 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
afterReadPromises: [blockAfterReadPromiseHOC(props)],
node: BlockNode,
type: BlockNode.getType(),
validations: [blockValidationHOC(props)],
},
],
plugins: [
@@ -74,7 +75,6 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
keywords: ['block', 'blocks', block.slug],
onSelect: ({ editor }) => {
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
collapsed: false,
data: {
blockName: '',
blockType: block.slug,

View File

@@ -11,17 +11,18 @@ import type {
} from 'lexical'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import ObjectID from 'bson-objectid'
import React from 'react'
import { BlockComponent } from '../component'
export type BlockFields = {
collapsed: boolean
/** Block data */
data: {
[key: string]: any
blockName: string
blockType: string
id?: string
}
}
@@ -118,9 +119,15 @@ export class BlockNode extends DecoratorBlockNode {
}
}
export function $createBlockNode(fields: BlockFields): BlockNode {
export function $createBlockNode(fields: Exclude<BlockFields, 'id'>): BlockNode {
return new BlockNode({
fields,
fields: {
...fields,
data: {
...fields.data,
id: fields?.data?.id || new ObjectID().toHexString(),
},
},
})
}

View File

@@ -0,0 +1,62 @@
import type { Block } from 'payload/types'
import { sanitizeFields } from 'payload/config'
import type { BlocksFeatureProps } from '.'
import type { NodeValidation } from '../types'
import type { SerializedBlockNode } from './nodes/BlocksNode'
export const blockValidationHOC = (
props: BlocksFeatureProps,
): NodeValidation<SerializedBlockNode> => {
const blockValidation: NodeValidation<SerializedBlockNode> = async ({
node,
nodeValidations,
payloadConfig,
validation,
}) => {
const blockFieldValues = node.fields.data
const blocks: Block[] = props.blocks
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
blocks.forEach((block) => {
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
block.fields = sanitizeFields({
config: payloadConfig,
fields: block.fields,
validRelationships,
})
})
// find block
const block = props.blocks.find((block) => block.slug === blockFieldValues.blockType)
// validate block
if (!block) {
return 'Block not found'
}
for (const field of block.fields) {
if ('validate' in field && typeof field.validate === 'function' && field.validate) {
const fieldValue = 'name' in field ? node.fields.data[field.name] : null
const validationResult = await field.validate(fieldValue, {
id: validation.options.id,
config: payloadConfig,
data: fieldValue,
operation: validation.options.operation,
siblingData: validation.options.siblingData,
t: validation.options.t,
user: validation.options.user,
})
if (validationResult !== true) {
return validationResult
}
}
}
return true
}
return blockValidation
}

View File

@@ -23,6 +23,7 @@ import {
useEditDepth,
useLocale,
} from 'payload/components/utilities'
import { sanitizeFields } from 'payload/config'
import { getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -65,7 +66,14 @@ export function LinkEditor({
const [initialState, setInitialState] = useState<Fields>({})
const [fieldSchema] = useState(() => {
const fields = transformExtraFields(customFieldSchema, config, i18n)
const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fields = sanitizeFields({
config: config,
fields: fieldsUnsanitized,
validRelationships,
})
return fields
})
@@ -132,6 +140,7 @@ export function LinkEditor({
// values saved in the link node you clicked on.
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data,
fieldSchema,
locale,

View File

@@ -8,10 +8,11 @@ import { Form, FormSubmit, RenderFields } from 'payload/components/forms'
import {
buildStateFromSchema,
useAuth,
useConfig,
useDocumentInfo,
useLocale,
} from 'payload/components/utilities'
import { fieldTypes } from 'payload/config'
import { fieldTypes, sanitizeFields } from 'payload/config'
import { deepCopyObject, getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -49,8 +50,18 @@ export const ExtraFieldsUploadDrawer: React.FC<
const { closeModal } = useModal()
const { getDocPreferences } = useDocumentInfo()
const [initialState, setInitialState] = useState({})
const fieldSchema = (editorConfig?.resolvedFeatureMap.get('upload')?.props as UploadFeatureProps)
?.collections?.[relatedCollection.slug]?.fields
const fieldSchemaUnsanitized = (
editorConfig?.resolvedFeatureMap.get('upload')?.props as UploadFeatureProps
)?.collections?.[relatedCollection.slug]?.fields
const config = useConfig()
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fieldSchema = sanitizeFields({
config: config,
fields: fieldSchemaUnsanitized,
validRelationships,
})
const handleUpdateEditData = useCallback(
(_, data) => {
@@ -72,9 +83,18 @@ export const ExtraFieldsUploadDrawer: React.FC<
)
useEffect(() => {
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fieldSchema = sanitizeFields({
config: config,
fields: fieldSchemaUnsanitized,
validRelationships,
})
const awaitInitialState = async () => {
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data: deepCopyObject(fields || {}),
fieldSchema,
locale,
@@ -87,7 +107,7 @@ export const ExtraFieldsUploadDrawer: React.FC<
}
void awaitInitialState()
}, [user, locale, t, getDocPreferences, fields, fieldSchema])
}, [user, locale, t, getDocPreferences, fields, fieldSchemaUnsanitized, config])
return (
<Drawer

View File

@@ -1,7 +1,8 @@
import type { Transformer } from '@lexical/markdown'
import type { Klass, LexicalEditor, LexicalNode } from 'lexical'
import type { Klass, LexicalEditor, LexicalNode, SerializedEditorState } from 'lexical'
import type { SerializedLexicalNode } from 'lexical'
import type { PayloadRequest, RichTextField } from 'payload/types'
import type { SanitizedConfig } from 'payload/config'
import type { PayloadRequest, RichTextField, ValidateOptions } from 'payload/types'
import type React from 'react'
import type { AdapterProps } from '../../types'
@@ -28,6 +29,22 @@ export type AfterReadPromise<T extends SerializedLexicalNode = SerializedLexical
req: PayloadRequest
showHiddenFields: boolean
}) => Promise<void>[]
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
node,
nodeValidations,
payloadConfig,
validation,
}: {
node: T
nodeValidations: Map<string, Array<NodeValidation>>
payloadConfig: SanitizedConfig
validation: {
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
value: SerializedEditorState
}
}) => Promise<string | true> | string | true
export type Feature = {
floatingSelectToolbar?: {
sections: FloatingToolbarSection[]
@@ -37,6 +54,7 @@ export type Feature = {
afterReadPromises?: Array<AfterReadPromise>
node: Klass<LexicalNode>
type: string
validations?: Array<NodeValidation>
}>
plugins?: Array<
| {
@@ -124,4 +142,6 @@ export type SanitizedFeatures = Required<
>
groupsWithOptions: SlashMenuGroup[]
}
/** The node types mapped to their validations */
validations: Map<string, Array<NodeValidation>>
}

View File

@@ -1 +1,39 @@
@import 'payload/scss';
.rich-text-lexical {
display: flex;
isolation: isolate;
&__wrap {
width: 100%;
position: relative;
}
&--read-only {
.editor-shell {
background: var(--theme-elevation-200);
color: var(--theme-elevation-450);
padding: base(0.5);
}
}
}
html[data-theme='light'] {
.rich-text-lexical {
&.error {
.editor-shell {
@include lightInputError;
}
}
}
}
html[data-theme='dark'] {
.rich-text-lexical {
&.error {
.editor-shell {
@include darkInputError;
}
}
}
}

View File

@@ -1,5 +1,6 @@
@import 'payload/scss';
.rich-text-lexical {
.editor-shell {
position: relative;
@@ -32,3 +33,4 @@
/* Make it behave more like a background element (no interaction) */
pointer-events: none;
}
}

View File

@@ -17,6 +17,7 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
dynamicOptions: [],
groupsWithOptions: [],
},
validations: new Map(),
}
features.forEach((feature) => {
@@ -26,6 +27,9 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
if (node?.afterReadPromises?.length) {
sanitized.afterReadPromises.set(node.type, node.afterReadPromises)
}
if (node?.validations?.length) {
sanitized.validations.set(node.type, node.validations)
}
})
}
if (feature.plugins?.length) {

View File

@@ -11,6 +11,7 @@ import { defaultEditorConfig, defaultSanitizedEditorConfig } from './field/lexic
import { sanitizeEditorConfig } from './field/lexical/config/sanitize'
import { cloneDeep } from './field/lexical/utils/cloneDeep'
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
import { richTextValidateHOC } from './validate'
export function lexicalEditor({
userConfig,
@@ -56,6 +57,9 @@ export function lexicalEditor({
return null
},
validate: richTextValidateHOC({
editorConfig: finalSanitizedEditorConfig,
}),
}
}

View File

@@ -27,3 +27,23 @@ export const defaultRichTextValue = {
version: 1,
},
}
export const defaultRichTextValueV2 = {
root: {
children: [
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: null,
format: '',
indent: 0,
type: 'root',
version: 1,
},
}

View File

@@ -1,18 +0,0 @@
import type { RichTextField, Validate } from 'payload/types'
import type { AdapterProps } from '../types'
import { defaultRichTextValue } from './defaultValue'
export const richTextValidate: Validate<unknown, unknown, RichTextField<AdapterProps>> = (
value,
{ required, t },
) => {
if (required) {
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue)
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true
return t('validation:required')
}
return true
}

View File

@@ -0,0 +1,50 @@
import type { SerializedEditorState } from 'lexical'
import type { RichTextField, Validate } from 'payload/types'
import type { SanitizedEditorConfig } from '../field/lexical/config/types'
import { defaultRichTextValue, defaultRichTextValueV2 } from '../populate/defaultValue'
import { validateNodes } from './validateNodes'
export const richTextValidateHOC = ({ editorConfig }: { editorConfig: SanitizedEditorConfig }) => {
const richTextValidate: Validate<
SerializedEditorState,
SerializedEditorState,
unknown,
RichTextField
> = async (value, options) => {
const { required, t } = options
if (required) {
if (
!value ||
!value?.root?.children ||
!value?.root?.children?.length ||
JSON.stringify(value) === JSON.stringify(defaultRichTextValue) ||
JSON.stringify(value) === JSON.stringify(defaultRichTextValueV2)
) {
return t('validation:required')
}
}
// Traverse through nodes and validate them. Just like a node can hook into the population process (e.g. link or relationship nodes),
// they can also hook into the validation process. E.g. a block node probably has fields with validation rules.
const rootNodes = value?.root?.children
if (rootNodes && Array.isArray(rootNodes) && rootNodes?.length) {
return await validateNodes({
nodeValidations: editorConfig.features.validations,
nodes: rootNodes,
payloadConfig: options.config,
validation: {
options,
value,
},
})
}
return true
}
return richTextValidate
}

View File

@@ -0,0 +1,55 @@
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { SanitizedConfig } from 'payload/config'
import type { RichTextField, ValidateOptions } from 'payload/types'
import type { NodeValidation } from '../field/features/types'
export async function validateNodes({
nodeValidations,
nodes,
payloadConfig,
validation: validationFromProps,
}: {
nodeValidations: Map<string, Array<NodeValidation>>
nodes: SerializedLexicalNode[]
payloadConfig: SanitizedConfig
validation: {
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
value: SerializedEditorState
}
}): Promise<string | true> {
for (const node of nodes) {
// Validate node
if (
nodeValidations &&
typeof nodeValidations?.has === 'function' &&
nodeValidations?.has(node.type)
) {
const validations = nodeValidations.get(node.type)
for (const validation of validations) {
const validationResult = await validation({
node,
nodeValidations,
payloadConfig,
validation: validationFromProps,
})
if (validationResult !== true) {
return validationResult
}
}
}
// Validate node's children
if ('children' in node && node?.children) {
const childrenValidationResult = await validateNodes({
nodeValidations,
nodes: node.children as SerializedLexicalNode[],
payloadConfig,
validation: validationFromProps,
})
if (childrenValidationResult !== true) {
return childrenValidationResult
}
}
}
return true
}

View File

@@ -4,10 +4,12 @@ import type { AdapterArguments } from '../types'
import { defaultRichTextValue } from './defaultValue'
export const richText: Validate<unknown, unknown, RichTextField<AdapterArguments>> = (
value,
{ required, t },
) => {
export const richTextValidate: Validate<
unknown,
unknown,
RichTextField<AdapterArguments>,
RichTextField<AdapterArguments>
> = (value, { required, t }) => {
if (required) {
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue)
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true

View File

@@ -15,7 +15,7 @@ import { Editable, Slate, withReact } from 'slate-react'
import type { ElementNode, FieldProps, RichTextElement, RichTextLeaf, TextNode } from '../types'
import { defaultRichTextValue } from '../data/defaultValue'
import { richText } from '../data/validation'
import { richTextValidate } from '../data/validation'
import elementTypes from './elements'
import listTypes from './elements/listTypes'
import enablePlugins from './enablePlugins'
@@ -80,7 +80,7 @@ const RichText: React.FC<FieldProps> = (props) => {
label,
path: pathFromProps,
required,
validate = richText,
validate = richTextValidate,
} = props
const elements: RichTextElement[] = admin?.elements || defaultElements

View File

@@ -10,6 +10,7 @@ import {
useDocumentInfo,
useLocale,
} from 'payload/components/utilities'
import { sanitizeFields } from 'payload/config'
import React, { Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Editor, Range, Transforms } from 'slate'
@@ -75,7 +76,14 @@ export const LinkButton: React.FC<{
const config = useConfig()
const [fieldSchema] = useState(() => {
const fields = transformExtraFields(customFieldSchema, config, i18n)
const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fields = sanitizeFields({
config: config,
fields: fieldsUnsanitized,
validRelationships,
})
return fields
})
@@ -104,6 +112,7 @@ export const LinkButton: React.FC<{
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data,
fieldSchema,
locale,

View File

@@ -12,6 +12,7 @@ import {
useDocumentInfo,
useLocale,
} from 'payload/components/utilities'
import { sanitizeFields } from 'payload/config'
import { deepCopyObject, getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
@@ -77,7 +78,14 @@ export const LinkElement: React.FC<{
const [initialState, setInitialState] = useState<Fields>({})
const { getDocPreferences } = useDocumentInfo()
const [fieldSchema] = useState(() => {
const fields = transformExtraFields(customFieldSchema, config, i18n)
const fieldsUnsanitized = transformExtraFields(customFieldSchema, config, i18n)
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fields = sanitizeFields({
config: config,
fields: fieldsUnsanitized,
validRelationships,
})
return fields
})
@@ -103,6 +111,7 @@ export const LinkElement: React.FC<{
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data,
fieldSchema,
locale,

View File

@@ -6,10 +6,11 @@ import { Form, FormSubmit, RenderFields } from 'payload/components/forms'
import {
buildStateFromSchema,
useAuth,
useConfig,
useDocumentInfo,
useLocale,
} from 'payload/components/utilities'
import { fieldTypes } from 'payload/config'
import { fieldTypes, sanitizeFields } from 'payload/config'
import { deepCopyObject, getTranslation } from 'payload/utilities'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -34,7 +35,17 @@ export const UploadDrawer: React.FC<
const { closeModal } = useModal()
const { getDocPreferences } = useDocumentInfo()
const [initialState, setInitialState] = useState({})
const fieldSchema = fieldProps?.admin?.upload?.collections?.[relatedCollection.slug]?.fields
const fieldSchemaUnsanitized =
fieldProps?.admin?.upload?.collections?.[relatedCollection.slug]?.fields
const config = useConfig()
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fieldSchema = sanitizeFields({
config: config,
fields: fieldSchemaUnsanitized,
validRelationships,
})
const handleUpdateEditData = useCallback(
(_, data) => {
@@ -51,9 +62,18 @@ export const UploadDrawer: React.FC<
)
useEffect(() => {
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fieldSchema = sanitizeFields({
config: config,
fields: fieldSchemaUnsanitized,
validRelationships,
})
const awaitInitialState = async () => {
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data: deepCopyObject(element?.fields || {}),
fieldSchema,
locale,
@@ -66,7 +86,7 @@ export const UploadDrawer: React.FC<
}
awaitInitialState()
}, [fieldSchema, element.fields, user, locale, t, getDocPreferences])
}, [fieldSchemaUnsanitized, config, element.fields, user, locale, t, getDocPreferences])
return (
<Drawer

View File

@@ -6,6 +6,7 @@ import type { AdapterArguments } from './types'
import RichTextCell from './cell'
import { richTextRelationshipPromise } from './data/richTextRelationshipPromise'
import { richTextValidate } from './data/validation'
import RichTextField from './field'
export function slateEditor(args: AdapterArguments): RichTextAdapter<AdapterArguments> {
@@ -45,5 +46,6 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<AdapterArgu
}
return null
},
validate: richTextValidate,
}
}

47
pnpm-lock.yaml generated
View File

@@ -115,8 +115,8 @@ importers:
specifier: 10.9.1
version: 10.9.1(@swc/core@1.3.76)(@types/node@20.5.7)(typescript@5.2.2)
turbo:
specifier: ^1.10.13
version: 1.10.14
specifier: ^1.10.15
version: 1.10.15
typescript:
specifier: 5.2.2
version: 5.2.2
@@ -961,6 +961,9 @@ importers:
'@lexical/utils':
specifier: 0.12.2
version: 0.12.2(lexical@0.12.2)
bson-objectid:
specifier: 2.0.4
version: 2.0.4
classnames:
specifier: ^2.3.2
version: 2.3.2
@@ -14705,64 +14708,64 @@ packages:
safe-buffer: 5.2.1
dev: false
/turbo-darwin-64@1.10.14:
resolution: {integrity: sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==}
/turbo-darwin-64@1.10.15:
resolution: {integrity: sha512-Sik5uogjkRTe1XVP9TC2GryEMOJCaKE2pM/O9uLn4koQDnWKGcLQv+mDU+H+9DXvKLnJnKCD18OVRkwK5tdpoA==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-darwin-arm64@1.10.14:
resolution: {integrity: sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==}
/turbo-darwin-arm64@1.10.15:
resolution: {integrity: sha512-xwqyFDYUcl2xwXyGPmHkmgnNm4Cy0oNzMpMOBGRr5x64SErS7QQLR4VHb0ubiR+VAb8M+ECPklU6vD1Gm+wekg==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-linux-64@1.10.14:
resolution: {integrity: sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==}
/turbo-linux-64@1.10.15:
resolution: {integrity: sha512-dM07SiO3RMAJ09Z+uB2LNUSkPp3I1IMF8goH5eLj+d8Kkwoxd/+qbUZOj9RvInyxU/IhlnO9w3PGd3Hp14m/nA==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-linux-arm64@1.10.14:
resolution: {integrity: sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==}
/turbo-linux-arm64@1.10.15:
resolution: {integrity: sha512-MkzKLkKYKyrz4lwfjNXH8aTny5+Hmiu4SFBZbx+5C0vOlyp6fV5jZANDBvLXWiDDL4DSEAuCEK/2cmN6FVH1ow==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-windows-64@1.10.14:
resolution: {integrity: sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==}
/turbo-windows-64@1.10.15:
resolution: {integrity: sha512-3TdVU+WEH9ThvQGwV3ieX/XHebtYNHv9HARHauPwmVj3kakoALkpGxLclkHFBLdLKkqDvmHmXtcsfs6cXXRHJg==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo-windows-arm64@1.10.14:
resolution: {integrity: sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==}
/turbo-windows-arm64@1.10.15:
resolution: {integrity: sha512-l+7UOBCbfadvPMYsX08hyLD+UIoAkg6ojfH+E8aud3gcA1padpjCJTh9gMpm3QdMbKwZteT5uUM+wyi6Rbbyww==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo@1.10.14:
resolution: {integrity: sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==}
/turbo@1.10.15:
resolution: {integrity: sha512-mKKkqsuDAQy1wCCIjCdG+jOCwUflhckDMSRoeBPcIL/CnCl7c5yRDFe7SyaXloUUkt4tUR0rvNIhVCcT7YeQpg==}
hasBin: true
optionalDependencies:
turbo-darwin-64: 1.10.14
turbo-darwin-arm64: 1.10.14
turbo-linux-64: 1.10.14
turbo-linux-arm64: 1.10.14
turbo-windows-64: 1.10.14
turbo-windows-arm64: 1.10.14
turbo-darwin-64: 1.10.15
turbo-darwin-arm64: 1.10.15
turbo-linux-64: 1.10.15
turbo-linux-arm64: 1.10.15
turbo-windows-64: 1.10.15
turbo-windows-arm64: 1.10.15
dev: true
/type-check@0.4.0:

View File

@@ -29,6 +29,7 @@ const RichTextFields: CollectionConfig = {
{
name: 'richTextLexicalCustomFields',
type: 'richText',
required: true,
editor: lexicalEditor({
userConfig(defaultEditorConfig) {
defaultEditorConfig.features.push(TreeviewFeature())
@@ -298,6 +299,7 @@ const RichTextFields: CollectionConfig = {
export const richTextBulletsDoc = {
title: 'Bullets and Indentation',
richTextLexicalCustomFields: generateLexicalRichText(),
richText: [
{
type: 'ul',