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:
@@ -65,7 +65,7 @@
|
|||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"shelljs": "0.8.5",
|
"shelljs": "0.8.5",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"turbo": "^1.10.13",
|
"turbo": "^1.10.15",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
|
|||||||
const hasInitializedState = useRef(false)
|
const hasInitializedState = useRef(false)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [collectionConfig] = useRelatedCollections(collectionSlug)
|
const [collectionConfig] = useRelatedCollections(collectionSlug)
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
const { admin: { components: { views: { Edit } = {} } = {} } = {} } = collectionConfig
|
const { admin: { components: { views: { Edit } = {} } = {} } = {} } = collectionConfig
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
|
|||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema: fields,
|
fieldSchema: fields,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { TFunction } from 'i18next'
|
|||||||
import ObjectID from 'bson-objectid'
|
import ObjectID from 'bson-objectid'
|
||||||
|
|
||||||
import type { User } from '../../../../../auth'
|
import type { User } from '../../../../../auth'
|
||||||
|
import type { SanitizedConfig } from '../../../../../config/types'
|
||||||
import type { NonPresentationalField } from '../../../../../fields/config/types'
|
import type { NonPresentationalField } from '../../../../../fields/config/types'
|
||||||
import type { Data, Fields, FormField } from '../types'
|
import type { Data, Fields, FormField } from '../types'
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import getValueWithDefault from '../../../../../fields/getDefaultValue'
|
|||||||
import { iterateFields } from './iterateFields'
|
import { iterateFields } from './iterateFields'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
config: SanitizedConfig
|
||||||
data: Data
|
data: Data
|
||||||
field: NonPresentationalField
|
field: NonPresentationalField
|
||||||
fullData: Data
|
fullData: Data
|
||||||
@@ -30,6 +32,7 @@ type Args = {
|
|||||||
|
|
||||||
export const addFieldStatePromise = async ({
|
export const addFieldStatePromise = async ({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
field,
|
field,
|
||||||
fullData,
|
fullData,
|
||||||
@@ -68,6 +71,7 @@ export const addFieldStatePromise = async ({
|
|||||||
validationResult = await fieldState.validate(data?.[field.name], {
|
validationResult = await fieldState.validate(data?.[field.name], {
|
||||||
...field,
|
...field,
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: fullData,
|
data: fullData,
|
||||||
operation,
|
operation,
|
||||||
siblingData: data,
|
siblingData: data,
|
||||||
@@ -100,6 +104,7 @@ export const addFieldStatePromise = async ({
|
|||||||
acc.promises.push(
|
acc.promises.push(
|
||||||
iterateFields({
|
iterateFields({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: row,
|
data: row,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
fullData,
|
fullData,
|
||||||
@@ -188,6 +193,7 @@ export const addFieldStatePromise = async ({
|
|||||||
acc.promises.push(
|
acc.promises.push(
|
||||||
iterateFields({
|
iterateFields({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: row,
|
data: row,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
fullData,
|
fullData,
|
||||||
@@ -249,6 +255,7 @@ export const addFieldStatePromise = async ({
|
|||||||
case 'group': {
|
case 'group': {
|
||||||
await iterateFields({
|
await iterateFields({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: data?.[field.name] || {},
|
data: data?.[field.name] || {},
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
fullData,
|
fullData,
|
||||||
@@ -348,6 +355,7 @@ export const addFieldStatePromise = async ({
|
|||||||
// Handle field types that do not use names (row, etc)
|
// Handle field types that do not use names (row, etc)
|
||||||
await iterateFields({
|
await iterateFields({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
fullData,
|
fullData,
|
||||||
@@ -364,6 +372,7 @@ export const addFieldStatePromise = async ({
|
|||||||
const promises = field.tabs.map((tab) =>
|
const promises = field.tabs.map((tab) =>
|
||||||
iterateFields({
|
iterateFields({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: tabHasName(tab) ? data?.[tab.name] : data,
|
data: tabHasName(tab) ? data?.[tab.name] : data,
|
||||||
fields: tab.fields,
|
fields: tab.fields,
|
||||||
fullData,
|
fullData,
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { TFunction } from 'i18next'
|
import type { TFunction } from 'i18next'
|
||||||
|
|
||||||
import type { User } from '../../../../../auth'
|
import type { User } from '../../../../../auth'
|
||||||
|
import type { SanitizedConfig } from '../../../../../config/types'
|
||||||
import type { Field as FieldSchema } from '../../../../../fields/config/types'
|
import type { Field as FieldSchema } from '../../../../../fields/config/types'
|
||||||
import type { Data, Fields } from '../types'
|
import type { Data, Fields } from '../types'
|
||||||
|
|
||||||
import { iterateFields } from './iterateFields'
|
import { iterateFields } from './iterateFields'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
config: SanitizedConfig
|
||||||
data?: Data
|
data?: Data
|
||||||
fieldSchema: FieldSchema[]
|
fieldSchema: FieldSchema[]
|
||||||
id?: number | string
|
id?: number | string
|
||||||
@@ -21,13 +23,24 @@ type Args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buildStateFromSchema = async (args: Args): Promise<Fields> => {
|
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) {
|
if (fieldSchema) {
|
||||||
const state: Fields = {}
|
const state: Fields = {}
|
||||||
|
|
||||||
await iterateFields({
|
await iterateFields({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: fullData,
|
data: fullData,
|
||||||
fields: fieldSchema,
|
fields: fieldSchema,
|
||||||
fullData,
|
fullData,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TFunction } from 'i18next'
|
import type { TFunction } from 'i18next'
|
||||||
|
|
||||||
import type { User } from '../../../../../auth'
|
import type { User } from '../../../../../auth'
|
||||||
|
import type { SanitizedConfig } from '../../../../../config/types'
|
||||||
import type { Field as FieldSchema } from '../../../../../fields/config/types'
|
import type { Field as FieldSchema } from '../../../../../fields/config/types'
|
||||||
import type { Data, Fields } from '../types'
|
import type { Data, Fields } from '../types'
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ import { fieldIsPresentationalOnly } from '../../../../../fields/config/types'
|
|||||||
import { addFieldStatePromise } from './addFieldStatePromise'
|
import { addFieldStatePromise } from './addFieldStatePromise'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
config: SanitizedConfig
|
||||||
data: Data
|
data: Data
|
||||||
fields: FieldSchema[]
|
fields: FieldSchema[]
|
||||||
fullData: Data
|
fullData: Data
|
||||||
@@ -26,6 +28,7 @@ type Args = {
|
|||||||
|
|
||||||
export const iterateFields = async ({
|
export const iterateFields = async ({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fields,
|
fields,
|
||||||
fullData,
|
fullData,
|
||||||
@@ -51,6 +54,7 @@ export const iterateFields = async ({
|
|||||||
promises.push(
|
promises.push(
|
||||||
addFieldStatePromise({
|
addFieldStatePromise({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
field,
|
field,
|
||||||
fullData,
|
fullData,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import wait from '../../../../utilities/wait'
|
|||||||
import { requests } from '../../../api'
|
import { requests } from '../../../api'
|
||||||
import useThrottledEffect from '../../../hooks/useThrottledEffect'
|
import useThrottledEffect from '../../../hooks/useThrottledEffect'
|
||||||
import { useAuth } from '../../utilities/Auth'
|
import { useAuth } from '../../utilities/Auth'
|
||||||
|
import { useConfig } from '../../utilities/Config'
|
||||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||||
import { useLocale } from '../../utilities/Locale'
|
import { useLocale } from '../../utilities/Locale'
|
||||||
import { useOperation } from '../../utilities/OperationProvider'
|
import { useOperation } from '../../utilities/OperationProvider'
|
||||||
@@ -62,6 +63,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
redirect,
|
redirect,
|
||||||
|
submitted: submittedFromProps,
|
||||||
waitForAutocomplete,
|
waitForAutocomplete,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
@@ -72,6 +74,8 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||||
const operation = useOperation()
|
const operation = useOperation()
|
||||||
|
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
const [modified, setModified] = useState(false)
|
const [modified, setModified] = useState(false)
|
||||||
const [processing, setProcessing] = useState(false)
|
const [processing, setProcessing] = useState(false)
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
@@ -165,6 +169,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
if (typeof field.validate === 'function') {
|
if (typeof field.validate === 'function') {
|
||||||
validationResult = await field.validate(field.value, {
|
validationResult = await field.validate(field.value, {
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
operation,
|
operation,
|
||||||
siblingData: contextRef.current.getSiblingData(path),
|
siblingData: contextRef.current.getSiblingData(path),
|
||||||
@@ -191,7 +196,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return isValid
|
return isValid
|
||||||
}, [contextRef, id, user, operation, t, dispatchFields])
|
}, [contextRef, id, user, operation, t, dispatchFields, config])
|
||||||
|
|
||||||
const submit = useCallback(
|
const submit = useCallback(
|
||||||
async (options: SubmitOptions = {}, e): Promise<void> => {
|
async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||||
@@ -452,6 +457,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
if (fieldConfig) {
|
if (fieldConfig) {
|
||||||
const subFieldState = await buildStateFromSchema({
|
const subFieldState = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema: fieldConfig,
|
fieldSchema: fieldConfig,
|
||||||
locale,
|
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(
|
const removeFieldRow: Context['removeFieldRow'] = useCallback(
|
||||||
@@ -490,6 +496,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
if (fieldConfig) {
|
if (fieldConfig) {
|
||||||
const subFieldState = await buildStateFromSchema({
|
const subFieldState = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema: fieldConfig,
|
fieldSchema: fieldConfig,
|
||||||
locale,
|
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])
|
const getFields = useCallback(() => contextRef.current.fields, [contextRef])
|
||||||
@@ -557,6 +564,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
@@ -569,7 +577,7 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
setModified(false)
|
setModified(false)
|
||||||
dispatchFields({ state, type: 'REPLACE_STATE' })
|
dispatchFields({ state, type: 'REPLACE_STATE' })
|
||||||
},
|
},
|
||||||
[id, user, operation, locale, t, dispatchFields, getDocPreferences],
|
[id, user, operation, locale, t, dispatchFields, getDocPreferences, config],
|
||||||
)
|
)
|
||||||
|
|
||||||
const replaceState = useCallback(
|
const replaceState = useCallback(
|
||||||
@@ -601,6 +609,10 @@ const Form: React.FC<Props> = (props) => {
|
|||||||
contextRef.current.removeFieldRow = removeFieldRow
|
contextRef.current.removeFieldRow = removeFieldRow
|
||||||
contextRef.current.replaceFieldRow = replaceFieldRow
|
contextRef.current.replaceFieldRow = replaceFieldRow
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof submittedFromProps === 'boolean') setSubmitted(submittedFromProps)
|
||||||
|
}, [submittedFromProps])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
contextRef.current = { ...initContextState } as FormContextType
|
contextRef.current = { ...initContextState } as FormContextType
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type Props = {
|
|||||||
onSubmit?: (fields: Fields, data: Data) => void
|
onSubmit?: (fields: Fields, data: Data) => void
|
||||||
onSuccess?: (json: unknown) => void
|
onSuccess?: (json: unknown) => void
|
||||||
redirect?: string
|
redirect?: string
|
||||||
|
submitted?: boolean
|
||||||
validationOperation?: 'create' | 'update'
|
validationOperation?: 'create' | 'update'
|
||||||
waitForAutocomplete?: boolean
|
waitForAutocomplete?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,9 @@ import React from 'react'
|
|||||||
import type { RichTextField } from '../../../../../fields/config/types'
|
import type { RichTextField } from '../../../../../fields/config/types'
|
||||||
import type { RichTextAdapter } from './types'
|
import type { RichTextAdapter } from './types'
|
||||||
|
|
||||||
import { useConfig } from '../../../utilities/Config'
|
|
||||||
|
|
||||||
const RichText: React.FC<RichTextField> = (props) => {
|
const RichText: React.FC<RichTextField> = (props) => {
|
||||||
const config = useConfig()
|
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
// eslint-disable-next-line react/destructuring-assignment
|
||||||
const editor: RichTextAdapter = props.editor || config.editor
|
const editor: RichTextAdapter = props.editor
|
||||||
return <editor.FieldComponent {...props} />
|
return <editor.FieldComponent {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PayloadRequest } from '../../../../../express/types'
|
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'
|
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
|
||||||
|
|
||||||
export type RichTextFieldProps<AdapterProps = unknown> = Omit<
|
export type RichTextFieldProps<AdapterProps = unknown> = Omit<
|
||||||
@@ -21,4 +21,5 @@ export type RichTextAdapter<AdapterProps = unknown> = {
|
|||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
siblingDoc: Record<string, unknown>
|
siblingDoc: Record<string, unknown>
|
||||||
}) => Promise<void> | null
|
}) => Promise<void> | null
|
||||||
|
validate: Validate<unknown, unknown, RichTextField<AdapterProps>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { FieldType, Options } from './types'
|
|||||||
|
|
||||||
import useThrottledEffect from '../../../hooks/useThrottledEffect'
|
import useThrottledEffect from '../../../hooks/useThrottledEffect'
|
||||||
import { useAuth } from '../../utilities/Auth'
|
import { useAuth } from '../../utilities/Auth'
|
||||||
|
import { useConfig } from '../../utilities/Config'
|
||||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||||
import { useOperation } from '../../utilities/OperationProvider'
|
import { useOperation } from '../../utilities/OperationProvider'
|
||||||
import { useForm, useFormFields, useFormProcessing, useFormSubmitted } from '../Form/context'
|
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 field = useFormFields(([fields]) => fields[path])
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
const { getData, getSiblingData, setModified } = useForm()
|
const { getData, getSiblingData, setModified } = useForm()
|
||||||
|
|
||||||
@@ -106,6 +108,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
|||||||
|
|
||||||
const validateOptions = {
|
const validateOptions = {
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: getData(),
|
data: getData(),
|
||||||
operation,
|
operation,
|
||||||
siblingData: getSiblingData(path),
|
siblingData: getSiblingData(path),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const AccountView: React.FC = () => {
|
|||||||
const { id, docPermissions, getDocPermissions, getDocPreferences, preferencesKey, slug } =
|
const { id, docPermissions, getDocPermissions, getDocPreferences, preferencesKey, slug } =
|
||||||
useDocumentInfo()
|
useDocumentInfo()
|
||||||
const { getPreference } = usePreferences()
|
const { getPreference } = usePreferences()
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: {
|
admin: {
|
||||||
@@ -65,6 +66,7 @@ const AccountView: React.FC = () => {
|
|||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: json.doc,
|
data: json.doc,
|
||||||
fieldSchema: collection.fields,
|
fieldSchema: collection.fields,
|
||||||
locale,
|
locale,
|
||||||
@@ -94,6 +96,7 @@ const AccountView: React.FC = () => {
|
|||||||
|
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: dataToRender,
|
data: dataToRender,
|
||||||
fieldSchema: fields,
|
fieldSchema: fields,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
useDocumentInfo()
|
useDocumentInfo()
|
||||||
const { getPreference } = usePreferences()
|
const { getPreference } = usePreferences()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routes: { api },
|
routes: { api },
|
||||||
@@ -44,6 +45,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
setUpdatedAt(json?.result?.updatedAt)
|
setUpdatedAt(json?.result?.updatedAt)
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data: json.result,
|
data: json.result,
|
||||||
fieldSchema: fields,
|
fieldSchema: fields,
|
||||||
locale,
|
locale,
|
||||||
@@ -68,6 +70,7 @@ const GlobalView: React.FC<IndexProps> = (props) => {
|
|||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data: dataToRender,
|
data: dataToRender,
|
||||||
fieldSchema: fields,
|
fieldSchema: fields,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
|
|
||||||
const { code: locale } = useLocale()
|
const { code: locale } = useLocale()
|
||||||
|
|
||||||
|
const config = useConfig()
|
||||||
const {
|
const {
|
||||||
routes: { admin, api },
|
routes: { admin, api },
|
||||||
serverURL,
|
serverURL,
|
||||||
} = useConfig()
|
} = config
|
||||||
|
|
||||||
const { params: { id } = {} } = useRouteMatch<Record<string, string>>()
|
const { params: { id } = {} } = useRouteMatch<Record<string, string>>()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
@@ -56,6 +57,7 @@ const EditView: React.FC<IndexProps> = (props) => {
|
|||||||
|
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
id,
|
id,
|
||||||
|
config,
|
||||||
data: doc || {},
|
data: doc || {},
|
||||||
fieldSchema: overrides.fieldSchema,
|
fieldSchema: overrides.fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ import type { RichTextField } from '../../../../../../../../fields/config/types'
|
|||||||
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
|
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
|
||||||
import type { CellComponentProps } from '../../types'
|
import type { CellComponentProps } from '../../types'
|
||||||
|
|
||||||
import { useConfig } from '../../../../../../utilities/Config'
|
|
||||||
|
|
||||||
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
|
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
|
||||||
const config = useConfig()
|
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
// 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} />
|
return <editor.CellComponent {...props} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default joi.object({
|
|||||||
CellComponent: component.required(),
|
CellComponent: component.required(),
|
||||||
FieldComponent: component.required(),
|
FieldComponent: component.required(),
|
||||||
afterReadPromise: joi.func().required(),
|
afterReadPromise: joi.func().required(),
|
||||||
|
validate: joi.func().required(),
|
||||||
}),
|
}),
|
||||||
email: joi.object(),
|
email: joi.object(),
|
||||||
endpoints: endpointsSchema,
|
endpoints: endpointsSchema,
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi
|
|||||||
throw new InvalidFieldName(field, field.name)
|
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
|
// Auto-label
|
||||||
if (
|
if (
|
||||||
'name' in field &&
|
'name' in field &&
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ export const richText = baseField.keys({
|
|||||||
CellComponent: componentSchema.required(),
|
CellComponent: componentSchema.required(),
|
||||||
FieldComponent: componentSchema.required(),
|
FieldComponent: componentSchema.required(),
|
||||||
afterReadPromise: joi.func().required(),
|
afterReadPromise: joi.func().required(),
|
||||||
|
validate: joi.func().required(),
|
||||||
}),
|
}),
|
||||||
type: joi.string().valid('richText').required(),
|
type: joi.string().valid('richText').required(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 { RichTextAdapter } from '../../admin/components/forms/field-types/RichText/types'
|
||||||
import type { User } from '../../auth'
|
import type { User } from '../../auth'
|
||||||
import type { TypeWithID } from '../../collections/config/types'
|
import type { TypeWithID } from '../../collections/config/types'
|
||||||
|
import type { SanitizedConfig } from '../../config/types'
|
||||||
import type { PayloadRequest, RequestContext } from '../../express/types'
|
import type { PayloadRequest, RequestContext } from '../../express/types'
|
||||||
import type { Payload } from '../../payload'
|
import type { Payload } from '../../payload'
|
||||||
import type { Operation, Where } from '../../types'
|
import type { Operation, Where } from '../../types'
|
||||||
@@ -91,6 +92,7 @@ export type Labels = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
||||||
|
config: SanitizedConfig
|
||||||
data: Partial<TData>
|
data: Partial<TData>
|
||||||
id?: number | string
|
id?: number | string
|
||||||
operation?: Operation
|
operation?: Operation
|
||||||
@@ -100,6 +102,7 @@ export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
|||||||
user?: Partial<User>
|
user?: Partial<User>
|
||||||
} & TFieldConfig
|
} & 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> = (
|
export type Validate<TValue = any, TData = any, TSiblingData = any, TFieldConfig = any> = (
|
||||||
value: TValue,
|
value: TValue,
|
||||||
options: ValidateOptions<TData, TSiblingData, TFieldConfig>,
|
options: ValidateOptions<TData, TSiblingData, TFieldConfig>,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const promise = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'richText': {
|
case 'richText': {
|
||||||
const editor: RichTextAdapter = field?.editor || req?.payload?.config?.editor
|
const editor: RichTextAdapter = field?.editor
|
||||||
if (editor?.afterReadPromise) {
|
if (editor?.afterReadPromise) {
|
||||||
const afterReadPromise = editor.afterReadPromise({
|
const afterReadPromise = editor.afterReadPromise({
|
||||||
currentDepth,
|
currentDepth,
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const promise = async ({
|
|||||||
const validationResult = await field.validate(valueToValidate, {
|
const validationResult = await field.validate(valueToValidate, {
|
||||||
...field,
|
...field,
|
||||||
id,
|
id,
|
||||||
|
config: req.payload.config,
|
||||||
data: merge(doc, data, { arrayMerge: (_, source) => source }),
|
data: merge(doc, data, { arrayMerge: (_, source) => source }),
|
||||||
jsonError,
|
jsonError,
|
||||||
operation,
|
operation,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { RichTextAdapter } from '../exports/types'
|
||||||
import type {
|
import type {
|
||||||
ArrayField,
|
ArrayField,
|
||||||
BlockField,
|
BlockField,
|
||||||
@@ -11,6 +12,7 @@ import type {
|
|||||||
RadioField,
|
RadioField,
|
||||||
RelationshipField,
|
RelationshipField,
|
||||||
RelationshipValue,
|
RelationshipValue,
|
||||||
|
RichTextField,
|
||||||
SelectField,
|
SelectField,
|
||||||
TextField,
|
TextField,
|
||||||
TextareaField,
|
TextareaField,
|
||||||
@@ -212,6 +214,15 @@ export const date: Validate<unknown, unknown, DateField> = (value, { required, t
|
|||||||
return true
|
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 (
|
const validateFilterOptions: Validate = async (
|
||||||
value,
|
value,
|
||||||
{ id, data, filterOptions, payload, relationTo, siblingData, t, user },
|
{ id, data, filterOptions, payload, relationTo, siblingData, t, user },
|
||||||
@@ -511,6 +522,7 @@ export default {
|
|||||||
point,
|
point,
|
||||||
radio,
|
radio,
|
||||||
relationship,
|
relationship,
|
||||||
|
richText,
|
||||||
select,
|
select,
|
||||||
text,
|
text,
|
||||||
textarea,
|
textarea,
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ function buildObjectType({
|
|||||||
async resolve(parent, args, context) {
|
async resolve(parent, args, context) {
|
||||||
let depth = payload.config.defaultDepth
|
let depth = payload.config.defaultDepth
|
||||||
if (typeof args.depth !== 'undefined') depth = args.depth
|
if (typeof args.depth !== 'undefined') depth = args.depth
|
||||||
const editor: RichTextAdapter = field?.editor || payload?.config?.editor
|
const editor: RichTextAdapter = field?.editor
|
||||||
|
|
||||||
if (editor?.afterReadPromise) {
|
if (editor?.afterReadPromise) {
|
||||||
await editor?.afterReadPromise({
|
await editor?.afterReadPromise({
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@lexical/selection": "0.12.2",
|
"@lexical/selection": "0.12.2",
|
||||||
"@lexical/table": "0.12.2",
|
"@lexical/table": "0.12.2",
|
||||||
"@lexical/utils": "0.12.2",
|
"@lexical/utils": "0.12.2",
|
||||||
|
"bson-objectid": "2.0.4",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"i18next": "22.5.1",
|
"i18next": "22.5.1",
|
||||||
"katex": "0.16.8",
|
"katex": "0.16.8",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { ErrorBoundary } from 'react-error-boundary'
|
|||||||
|
|
||||||
import type { FieldProps } from '../types'
|
import type { FieldProps } from '../types'
|
||||||
|
|
||||||
import { richTextValidate } from '../populate/validation'
|
import { defaultRichTextValueV2 } from '../populate/defaultValue'
|
||||||
|
import { richTextValidateHOC } from '../validate'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { LexicalProvider } from './lexical/LexicalProvider'
|
import { LexicalProvider } from './lexical/LexicalProvider'
|
||||||
|
|
||||||
@@ -23,22 +24,21 @@ const RichText: React.FC<FieldProps> = (props) => {
|
|||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
},
|
},
|
||||||
admin,
|
|
||||||
defaultValue: defaultValueFromProps,
|
defaultValue: defaultValueFromProps,
|
||||||
editorConfig,
|
editorConfig,
|
||||||
label,
|
label,
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
validate = richTextValidate,
|
validate = richTextValidateHOC({ editorConfig }),
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const path = pathFromProps || name
|
||||||
|
|
||||||
const memoizedValidate = useCallback(
|
const memoizedValidate = useCallback(
|
||||||
(value, validationOptions) => {
|
(value, validationOptions) => {
|
||||||
return validate(value, { ...validationOptions, required })
|
return validate(value, { ...validationOptions, props, required })
|
||||||
},
|
},
|
||||||
[validate, required],
|
[validate, required, props],
|
||||||
)
|
)
|
||||||
|
|
||||||
const fieldType = useField<SerializedEditorState>({
|
const fieldType = useField<SerializedEditorState>({
|
||||||
@@ -49,6 +49,19 @@ const RichText: React.FC<FieldProps> = (props) => {
|
|||||||
|
|
||||||
const { errorMessage, initialValue, setValue, showError, value } = fieldType
|
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 = [
|
const classes = [
|
||||||
baseClass,
|
baseClass,
|
||||||
'field-type',
|
'field-type',
|
||||||
|
|||||||
@@ -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 { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { $getNodeByKey } from 'lexical'
|
import { $getNodeByKey } from 'lexical'
|
||||||
@@ -6,8 +6,9 @@ import { Button, ErrorPill, Pill } from 'payload/components'
|
|||||||
import { Collapsible } from 'payload/components/elements'
|
import { Collapsible } from 'payload/components/elements'
|
||||||
import { SectionTitle } from 'payload/components/fields/Blocks'
|
import { SectionTitle } from 'payload/components/fields/Blocks'
|
||||||
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
|
import { RenderFields, createNestedFieldPath, useFormSubmitted } from 'payload/components/forms'
|
||||||
|
import { useDocumentInfo } from 'payload/components/utilities'
|
||||||
import { getTranslation } from 'payload/utilities'
|
import { getTranslation } from 'payload/utilities'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import type { FieldProps } from '../../../../types'
|
import type { FieldProps } from '../../../../types'
|
||||||
@@ -27,10 +28,31 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
const { baseClass, block, field, fields, nodeKey } = props
|
const { baseClass, block, field, fields, nodeKey } = props
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const [editor] = useLexicalComposerContext()
|
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 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 = [
|
const classNames = [
|
||||||
`${baseClass}__row`,
|
`${baseClass}__row`,
|
||||||
@@ -42,31 +64,46 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
const path = '' as const
|
const path = '' as const
|
||||||
|
|
||||||
const onFormChange = useCallback(
|
const onFormChange = useCallback(
|
||||||
({ formData }: { formData: Data }) => {
|
({ fields: formFields, formData }: { fields: Fields; formData: Data }) => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
const node: BlockNode = $getNodeByKey(nodeKey)
|
||||||
if (node) {
|
if (node) {
|
||||||
node.setFields({
|
node.setFields({
|
||||||
collapsed: collapsed,
|
|
||||||
data: formData as any,
|
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(() => {
|
const onCollapsedChange = useCallback(() => {
|
||||||
editor.update(() => {
|
getDocPreferences().then((currentDocPreferences) => {
|
||||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
const currentFieldPreferences = currentDocPreferences?.fields[field.name]
|
||||||
if (node) {
|
|
||||||
node.setFields({
|
const collapsedMap: { [key: string]: boolean } = currentFieldPreferences?.collapsed
|
||||||
...node.getFields(),
|
|
||||||
collapsed: 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(() => {
|
const removeBlock = useCallback(() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
@@ -79,7 +116,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
<Collapsible
|
<Collapsible
|
||||||
className={classNames}
|
className={classNames}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
collapsibleStyle={false ? 'error' : 'default'}
|
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||||
header={
|
header={
|
||||||
<div className={`${baseClass}__block-header`}>
|
<div className={`${baseClass}__block-header`}>
|
||||||
<div>
|
<div>
|
||||||
@@ -90,7 +127,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
{getTranslation(block.labels.singular, i18n)}
|
{getTranslation(block.labels.singular, i18n)}
|
||||||
</Pill>
|
</Pill>
|
||||||
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
||||||
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage />}
|
{fieldHasErrors && <ErrorPill count={errorCount} withMessage />}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="icon-label"
|
buttonStyle="icon-label"
|
||||||
|
|||||||
@@ -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 type React from 'react'
|
||||||
|
|
||||||
import { reduceFieldsToValues, useAllFormFields } from 'payload/components/forms'
|
import { reduceFieldsToValues, useAllFormFields } from 'payload/components/forms'
|
||||||
@@ -8,7 +8,7 @@ import './index.scss'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fieldSchema: FieldWithPath[]
|
fieldSchema: FieldWithPath[]
|
||||||
onChange?: ({ formData }: { formData: Data }) => void
|
onChange?: ({ fields, formData }: { fields: Fields; formData: Data }) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormSavePlugin: React.FC<Props> = (props) => {
|
export const FormSavePlugin: React.FC<Props> = (props) => {
|
||||||
@@ -22,9 +22,9 @@ export const FormSavePlugin: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange({ formData })
|
onChange({ fields, formData })
|
||||||
}
|
}
|
||||||
}, [formData, onChange])
|
}, [formData, onChange, fields])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type ElementFormatType } from 'lexical'
|
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 React, { useMemo } from 'react'
|
||||||
|
|
||||||
import { type BlockFields } from '../nodes/BlocksNode'
|
import { type BlockFields } from '../nodes/BlocksNode'
|
||||||
@@ -27,6 +27,7 @@ type Props = {
|
|||||||
export const BlockComponent: React.FC<Props> = (props) => {
|
export const BlockComponent: React.FC<Props> = (props) => {
|
||||||
const { children, className, fields, format, nodeKey } = props
|
const { children, className, fields, format, nodeKey } = props
|
||||||
const payloadConfig = useConfig()
|
const payloadConfig = useConfig()
|
||||||
|
const submitted = useFormSubmitted()
|
||||||
|
|
||||||
const { editorConfig, field } = useEditorConfigContext()
|
const { editorConfig, field } = useEditorConfigContext()
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
const formContent = useMemo(() => {
|
const formContent = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
block && (
|
block && (
|
||||||
<Form initialState={initialDataRef?.current}>
|
<Form initialState={initialDataRef?.current} submitted={submitted}>
|
||||||
<BlockContent
|
<BlockContent
|
||||||
baseClass={baseClass}
|
baseClass={baseClass}
|
||||||
block={block}
|
block={block}
|
||||||
@@ -59,7 +60,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
|||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [block, field, nodeKey])
|
}, [block, field, nodeKey, submitted])
|
||||||
|
|
||||||
return <div className={baseClass}>{formContent}</div>
|
return <div className={baseClass}>{formContent}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const insertBlock = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (!replaceNodeKey) {
|
if (!replaceNodeKey) {
|
||||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||||
collapsed: false,
|
|
||||||
data: {
|
data: {
|
||||||
blockName: '',
|
blockName: '',
|
||||||
blockType: blockType,
|
blockType: blockType,
|
||||||
@@ -48,7 +47,6 @@ const insertBlock = ({
|
|||||||
if (node) {
|
if (node) {
|
||||||
node.replace(
|
node.replace(
|
||||||
$createBlockNode({
|
$createBlockNode({
|
||||||
collapsed: false,
|
|
||||||
data: {
|
data: {
|
||||||
blockName: '',
|
blockName: '',
|
||||||
blockType: blockType,
|
blockType: blockType,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import type { FeatureProvider } from '../types'
|
|||||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu'
|
||||||
import { BlockIcon } from '../../lexical/ui/icons/Block'
|
import { BlockIcon } from '../../lexical/ui/icons/Block'
|
||||||
import { blockAfterReadPromiseHOC } from './afterReadPromise'
|
import { blockAfterReadPromiseHOC } from './afterReadPromise'
|
||||||
import { INSERT_BLOCK_WITH_DRAWER_COMMAND } from './drawer'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { BlockNode } from './nodes/BlocksNode'
|
import { BlockNode } from './nodes/BlocksNode'
|
||||||
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
|
import { BlocksPlugin, INSERT_BLOCK_COMMAND } from './plugin'
|
||||||
|
import { blockValidationHOC } from './validate'
|
||||||
|
|
||||||
export type BlocksFeatureProps = {
|
export type BlocksFeatureProps = {
|
||||||
blocks: Block[]
|
blocks: Block[]
|
||||||
@@ -45,6 +45,7 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
|||||||
afterReadPromises: [blockAfterReadPromiseHOC(props)],
|
afterReadPromises: [blockAfterReadPromiseHOC(props)],
|
||||||
node: BlockNode,
|
node: BlockNode,
|
||||||
type: BlockNode.getType(),
|
type: BlockNode.getType(),
|
||||||
|
validations: [blockValidationHOC(props)],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -74,7 +75,6 @@ export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
|||||||
keywords: ['block', 'blocks', block.slug],
|
keywords: ['block', 'blocks', block.slug],
|
||||||
onSelect: ({ editor }) => {
|
onSelect: ({ editor }) => {
|
||||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||||
collapsed: false,
|
|
||||||
data: {
|
data: {
|
||||||
blockName: '',
|
blockName: '',
|
||||||
blockType: block.slug,
|
blockType: block.slug,
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ import type {
|
|||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
|
|
||||||
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
|
import ObjectID from 'bson-objectid'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { BlockComponent } from '../component'
|
import { BlockComponent } from '../component'
|
||||||
|
|
||||||
export type BlockFields = {
|
export type BlockFields = {
|
||||||
collapsed: boolean
|
|
||||||
/** Block data */
|
/** Block data */
|
||||||
data: {
|
data: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
blockName: string
|
blockName: string
|
||||||
blockType: 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({
|
return new BlockNode({
|
||||||
fields,
|
fields: {
|
||||||
|
...fields,
|
||||||
|
data: {
|
||||||
|
...fields.data,
|
||||||
|
id: fields?.data?.id || new ObjectID().toHexString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
useEditDepth,
|
useEditDepth,
|
||||||
useLocale,
|
useLocale,
|
||||||
} from 'payload/components/utilities'
|
} from 'payload/components/utilities'
|
||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
import { getTranslation } from 'payload/utilities'
|
import { getTranslation } from 'payload/utilities'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -65,7 +66,14 @@ export function LinkEditor({
|
|||||||
const [initialState, setInitialState] = useState<Fields>({})
|
const [initialState, setInitialState] = useState<Fields>({})
|
||||||
|
|
||||||
const [fieldSchema] = useState(() => {
|
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
|
return fields
|
||||||
})
|
})
|
||||||
@@ -132,6 +140,7 @@ export function LinkEditor({
|
|||||||
// values saved in the link node you clicked on.
|
// values saved in the link node you clicked on.
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { Form, FormSubmit, RenderFields } from 'payload/components/forms'
|
|||||||
import {
|
import {
|
||||||
buildStateFromSchema,
|
buildStateFromSchema,
|
||||||
useAuth,
|
useAuth,
|
||||||
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useLocale,
|
useLocale,
|
||||||
} from 'payload/components/utilities'
|
} from 'payload/components/utilities'
|
||||||
import { fieldTypes } from 'payload/config'
|
import { fieldTypes, sanitizeFields } from 'payload/config'
|
||||||
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -49,8 +50,18 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
|||||||
const { closeModal } = useModal()
|
const { closeModal } = useModal()
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
const { getDocPreferences } = useDocumentInfo()
|
||||||
const [initialState, setInitialState] = useState({})
|
const [initialState, setInitialState] = useState({})
|
||||||
const fieldSchema = (editorConfig?.resolvedFeatureMap.get('upload')?.props as UploadFeatureProps)
|
const fieldSchemaUnsanitized = (
|
||||||
?.collections?.[relatedCollection.slug]?.fields
|
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(
|
const handleUpdateEditData = useCallback(
|
||||||
(_, data) => {
|
(_, data) => {
|
||||||
@@ -72,9 +83,18 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
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 awaitInitialState = async () => {
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data: deepCopyObject(fields || {}),
|
data: deepCopyObject(fields || {}),
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
@@ -87,7 +107,7 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
void awaitInitialState()
|
void awaitInitialState()
|
||||||
}, [user, locale, t, getDocPreferences, fields, fieldSchema])
|
}, [user, locale, t, getDocPreferences, fields, fieldSchemaUnsanitized, config])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { Transformer } from '@lexical/markdown'
|
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 { 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 React from 'react'
|
||||||
|
|
||||||
import type { AdapterProps } from '../../types'
|
import type { AdapterProps } from '../../types'
|
||||||
@@ -28,6 +29,22 @@ export type AfterReadPromise<T extends SerializedLexicalNode = SerializedLexical
|
|||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
showHiddenFields: boolean
|
showHiddenFields: boolean
|
||||||
}) => Promise<void>[]
|
}) => 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 = {
|
export type Feature = {
|
||||||
floatingSelectToolbar?: {
|
floatingSelectToolbar?: {
|
||||||
sections: FloatingToolbarSection[]
|
sections: FloatingToolbarSection[]
|
||||||
@@ -37,6 +54,7 @@ export type Feature = {
|
|||||||
afterReadPromises?: Array<AfterReadPromise>
|
afterReadPromises?: Array<AfterReadPromise>
|
||||||
node: Klass<LexicalNode>
|
node: Klass<LexicalNode>
|
||||||
type: string
|
type: string
|
||||||
|
validations?: Array<NodeValidation>
|
||||||
}>
|
}>
|
||||||
plugins?: Array<
|
plugins?: Array<
|
||||||
| {
|
| {
|
||||||
@@ -124,4 +142,6 @@ export type SanitizedFeatures = Required<
|
|||||||
>
|
>
|
||||||
groupsWithOptions: SlashMenuGroup[]
|
groupsWithOptions: SlashMenuGroup[]
|
||||||
}
|
}
|
||||||
|
/** The node types mapped to their validations */
|
||||||
|
validations: Map<string, Array<NodeValidation>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,39 @@
|
|||||||
@import 'payload/scss';
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@import 'payload/scss';
|
@import 'payload/scss';
|
||||||
|
|
||||||
|
.rich-text-lexical {
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -32,3 +33,4 @@
|
|||||||
/* Make it behave more like a background element (no interaction) */
|
/* Make it behave more like a background element (no interaction) */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
dynamicOptions: [],
|
dynamicOptions: [],
|
||||||
groupsWithOptions: [],
|
groupsWithOptions: [],
|
||||||
},
|
},
|
||||||
|
validations: new Map(),
|
||||||
}
|
}
|
||||||
|
|
||||||
features.forEach((feature) => {
|
features.forEach((feature) => {
|
||||||
@@ -26,6 +27,9 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
|||||||
if (node?.afterReadPromises?.length) {
|
if (node?.afterReadPromises?.length) {
|
||||||
sanitized.afterReadPromises.set(node.type, node.afterReadPromises)
|
sanitized.afterReadPromises.set(node.type, node.afterReadPromises)
|
||||||
}
|
}
|
||||||
|
if (node?.validations?.length) {
|
||||||
|
sanitized.validations.set(node.type, node.validations)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (feature.plugins?.length) {
|
if (feature.plugins?.length) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { defaultEditorConfig, defaultSanitizedEditorConfig } from './field/lexic
|
|||||||
import { sanitizeEditorConfig } from './field/lexical/config/sanitize'
|
import { sanitizeEditorConfig } from './field/lexical/config/sanitize'
|
||||||
import { cloneDeep } from './field/lexical/utils/cloneDeep'
|
import { cloneDeep } from './field/lexical/utils/cloneDeep'
|
||||||
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
|
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
|
||||||
|
import { richTextValidateHOC } from './validate'
|
||||||
|
|
||||||
export function lexicalEditor({
|
export function lexicalEditor({
|
||||||
userConfig,
|
userConfig,
|
||||||
@@ -56,6 +57,9 @@ export function lexicalEditor({
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
validate: richTextValidateHOC({
|
||||||
|
editorConfig: finalSanitizedEditorConfig,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,3 +27,23 @@ export const defaultRichTextValue = {
|
|||||||
version: 1,
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
50
packages/richtext-lexical/src/validate/index.ts
Normal file
50
packages/richtext-lexical/src/validate/index.ts
Normal 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
|
||||||
|
}
|
||||||
55
packages/richtext-lexical/src/validate/validateNodes.ts
Normal file
55
packages/richtext-lexical/src/validate/validateNodes.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import type { AdapterArguments } from '../types'
|
|||||||
|
|
||||||
import { defaultRichTextValue } from './defaultValue'
|
import { defaultRichTextValue } from './defaultValue'
|
||||||
|
|
||||||
export const richText: Validate<unknown, unknown, RichTextField<AdapterArguments>> = (
|
export const richTextValidate: Validate<
|
||||||
value,
|
unknown,
|
||||||
{ required, t },
|
unknown,
|
||||||
) => {
|
RichTextField<AdapterArguments>,
|
||||||
|
RichTextField<AdapterArguments>
|
||||||
|
> = (value, { required, t }) => {
|
||||||
if (required) {
|
if (required) {
|
||||||
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue)
|
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue)
|
||||||
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true
|
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Editable, Slate, withReact } from 'slate-react'
|
|||||||
import type { ElementNode, FieldProps, RichTextElement, RichTextLeaf, TextNode } from '../types'
|
import type { ElementNode, FieldProps, RichTextElement, RichTextLeaf, TextNode } from '../types'
|
||||||
|
|
||||||
import { defaultRichTextValue } from '../data/defaultValue'
|
import { defaultRichTextValue } from '../data/defaultValue'
|
||||||
import { richText } from '../data/validation'
|
import { richTextValidate } from '../data/validation'
|
||||||
import elementTypes from './elements'
|
import elementTypes from './elements'
|
||||||
import listTypes from './elements/listTypes'
|
import listTypes from './elements/listTypes'
|
||||||
import enablePlugins from './enablePlugins'
|
import enablePlugins from './enablePlugins'
|
||||||
@@ -80,7 +80,7 @@ const RichText: React.FC<FieldProps> = (props) => {
|
|||||||
label,
|
label,
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
validate = richText,
|
validate = richTextValidate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const elements: RichTextElement[] = admin?.elements || defaultElements
|
const elements: RichTextElement[] = admin?.elements || defaultElements
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useLocale,
|
useLocale,
|
||||||
} from 'payload/components/utilities'
|
} from 'payload/components/utilities'
|
||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
import React, { Fragment, useState } from 'react'
|
import React, { Fragment, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Editor, Range, Transforms } from 'slate'
|
import { Editor, Range, Transforms } from 'slate'
|
||||||
@@ -75,7 +76,14 @@ export const LinkButton: React.FC<{
|
|||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const [fieldSchema] = useState(() => {
|
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
|
return fields
|
||||||
})
|
})
|
||||||
@@ -104,6 +112,7 @@ export const LinkButton: React.FC<{
|
|||||||
|
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useLocale,
|
useLocale,
|
||||||
} from 'payload/components/utilities'
|
} from 'payload/components/utilities'
|
||||||
|
import { sanitizeFields } from 'payload/config'
|
||||||
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
@@ -77,7 +78,14 @@ export const LinkElement: React.FC<{
|
|||||||
const [initialState, setInitialState] = useState<Fields>({})
|
const [initialState, setInitialState] = useState<Fields>({})
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
const { getDocPreferences } = useDocumentInfo()
|
||||||
const [fieldSchema] = useState(() => {
|
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
|
return fields
|
||||||
})
|
})
|
||||||
@@ -103,6 +111,7 @@ export const LinkElement: React.FC<{
|
|||||||
|
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data,
|
data,
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Form, FormSubmit, RenderFields } from 'payload/components/forms'
|
|||||||
import {
|
import {
|
||||||
buildStateFromSchema,
|
buildStateFromSchema,
|
||||||
useAuth,
|
useAuth,
|
||||||
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useLocale,
|
useLocale,
|
||||||
} from 'payload/components/utilities'
|
} from 'payload/components/utilities'
|
||||||
import { fieldTypes } from 'payload/config'
|
import { fieldTypes, sanitizeFields } from 'payload/config'
|
||||||
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
import { deepCopyObject, getTranslation } from 'payload/utilities'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -34,7 +35,17 @@ export const UploadDrawer: React.FC<
|
|||||||
const { closeModal } = useModal()
|
const { closeModal } = useModal()
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
const { getDocPreferences } = useDocumentInfo()
|
||||||
const [initialState, setInitialState] = useState({})
|
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(
|
const handleUpdateEditData = useCallback(
|
||||||
(_, data) => {
|
(_, data) => {
|
||||||
@@ -51,9 +62,18 @@ export const UploadDrawer: React.FC<
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
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 awaitInitialState = async () => {
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
|
config,
|
||||||
data: deepCopyObject(element?.fields || {}),
|
data: deepCopyObject(element?.fields || {}),
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
@@ -66,7 +86,7 @@ export const UploadDrawer: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
awaitInitialState()
|
awaitInitialState()
|
||||||
}, [fieldSchema, element.fields, user, locale, t, getDocPreferences])
|
}, [fieldSchemaUnsanitized, config, element.fields, user, locale, t, getDocPreferences])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { AdapterArguments } 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 RichTextField from './field'
|
import RichTextField from './field'
|
||||||
|
|
||||||
export function slateEditor(args: AdapterArguments): RichTextAdapter<AdapterArguments> {
|
export function slateEditor(args: AdapterArguments): RichTextAdapter<AdapterArguments> {
|
||||||
@@ -45,5 +46,6 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<AdapterArgu
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
validate: richTextValidate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -115,8 +115,8 @@ importers:
|
|||||||
specifier: 10.9.1
|
specifier: 10.9.1
|
||||||
version: 10.9.1(@swc/core@1.3.76)(@types/node@20.5.7)(typescript@5.2.2)
|
version: 10.9.1(@swc/core@1.3.76)(@types/node@20.5.7)(typescript@5.2.2)
|
||||||
turbo:
|
turbo:
|
||||||
specifier: ^1.10.13
|
specifier: ^1.10.15
|
||||||
version: 1.10.14
|
version: 1.10.15
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.2.2
|
specifier: 5.2.2
|
||||||
version: 5.2.2
|
version: 5.2.2
|
||||||
@@ -961,6 +961,9 @@ importers:
|
|||||||
'@lexical/utils':
|
'@lexical/utils':
|
||||||
specifier: 0.12.2
|
specifier: 0.12.2
|
||||||
version: 0.12.2(lexical@0.12.2)
|
version: 0.12.2(lexical@0.12.2)
|
||||||
|
bson-objectid:
|
||||||
|
specifier: 2.0.4
|
||||||
|
version: 2.0.4
|
||||||
classnames:
|
classnames:
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
@@ -14705,64 +14708,64 @@ packages:
|
|||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/turbo-darwin-64@1.10.14:
|
/turbo-darwin-64@1.10.15:
|
||||||
resolution: {integrity: sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==}
|
resolution: {integrity: sha512-Sik5uogjkRTe1XVP9TC2GryEMOJCaKE2pM/O9uLn4koQDnWKGcLQv+mDU+H+9DXvKLnJnKCD18OVRkwK5tdpoA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/turbo-darwin-arm64@1.10.14:
|
/turbo-darwin-arm64@1.10.15:
|
||||||
resolution: {integrity: sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==}
|
resolution: {integrity: sha512-xwqyFDYUcl2xwXyGPmHkmgnNm4Cy0oNzMpMOBGRr5x64SErS7QQLR4VHb0ubiR+VAb8M+ECPklU6vD1Gm+wekg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/turbo-linux-64@1.10.14:
|
/turbo-linux-64@1.10.15:
|
||||||
resolution: {integrity: sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==}
|
resolution: {integrity: sha512-dM07SiO3RMAJ09Z+uB2LNUSkPp3I1IMF8goH5eLj+d8Kkwoxd/+qbUZOj9RvInyxU/IhlnO9w3PGd3Hp14m/nA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/turbo-linux-arm64@1.10.14:
|
/turbo-linux-arm64@1.10.15:
|
||||||
resolution: {integrity: sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==}
|
resolution: {integrity: sha512-MkzKLkKYKyrz4lwfjNXH8aTny5+Hmiu4SFBZbx+5C0vOlyp6fV5jZANDBvLXWiDDL4DSEAuCEK/2cmN6FVH1ow==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/turbo-windows-64@1.10.14:
|
/turbo-windows-64@1.10.15:
|
||||||
resolution: {integrity: sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==}
|
resolution: {integrity: sha512-3TdVU+WEH9ThvQGwV3ieX/XHebtYNHv9HARHauPwmVj3kakoALkpGxLclkHFBLdLKkqDvmHmXtcsfs6cXXRHJg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/turbo-windows-arm64@1.10.14:
|
/turbo-windows-arm64@1.10.15:
|
||||||
resolution: {integrity: sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==}
|
resolution: {integrity: sha512-l+7UOBCbfadvPMYsX08hyLD+UIoAkg6ojfH+E8aud3gcA1padpjCJTh9gMpm3QdMbKwZteT5uUM+wyi6Rbbyww==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/turbo@1.10.14:
|
/turbo@1.10.15:
|
||||||
resolution: {integrity: sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==}
|
resolution: {integrity: sha512-mKKkqsuDAQy1wCCIjCdG+jOCwUflhckDMSRoeBPcIL/CnCl7c5yRDFe7SyaXloUUkt4tUR0rvNIhVCcT7YeQpg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
turbo-darwin-64: 1.10.14
|
turbo-darwin-64: 1.10.15
|
||||||
turbo-darwin-arm64: 1.10.14
|
turbo-darwin-arm64: 1.10.15
|
||||||
turbo-linux-64: 1.10.14
|
turbo-linux-64: 1.10.15
|
||||||
turbo-linux-arm64: 1.10.14
|
turbo-linux-arm64: 1.10.15
|
||||||
turbo-windows-64: 1.10.14
|
turbo-windows-64: 1.10.15
|
||||||
turbo-windows-arm64: 1.10.14
|
turbo-windows-arm64: 1.10.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/type-check@0.4.0:
|
/type-check@0.4.0:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const RichTextFields: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'richTextLexicalCustomFields',
|
name: 'richTextLexicalCustomFields',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
required: true,
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
userConfig(defaultEditorConfig) {
|
userConfig(defaultEditorConfig) {
|
||||||
defaultEditorConfig.features.push(TreeviewFeature())
|
defaultEditorConfig.features.push(TreeviewFeature())
|
||||||
@@ -298,6 +299,7 @@ const RichTextFields: CollectionConfig = {
|
|||||||
|
|
||||||
export const richTextBulletsDoc = {
|
export const richTextBulletsDoc = {
|
||||||
title: 'Bullets and Indentation',
|
title: 'Bullets and Indentation',
|
||||||
|
richTextLexicalCustomFields: generateLexicalRichText(),
|
||||||
richText: [
|
richText: [
|
||||||
{
|
{
|
||||||
type: 'ul',
|
type: 'ul',
|
||||||
|
|||||||
Reference in New Issue
Block a user