feat(richtext-lexical): initial working BlocksFeature
This commit is contained in:
@@ -7,7 +7,6 @@ import { ErrorBoundary } from 'react-error-boundary'
|
|||||||
|
|
||||||
import type { SanitizedClientEditorConfig } from './lexical/config/types'
|
import type { SanitizedClientEditorConfig } from './lexical/config/types'
|
||||||
|
|
||||||
import { richTextValidateHOC } from '../validate'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { LexicalProvider } from './lexical/LexicalProvider'
|
import { LexicalProvider } from './lexical/LexicalProvider'
|
||||||
|
|
||||||
@@ -22,36 +21,24 @@ const RichText: React.FC<
|
|||||||
> = (props) => {
|
> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
AfterInput,
|
|
||||||
BeforeInput,
|
|
||||||
Description,
|
Description,
|
||||||
Error,
|
Error,
|
||||||
Label,
|
Label,
|
||||||
className,
|
className,
|
||||||
docPreferences,
|
|
||||||
editorConfig,
|
editorConfig,
|
||||||
fieldMap,
|
|
||||||
initialSubfieldState,
|
|
||||||
label,
|
|
||||||
locale,
|
|
||||||
localized,
|
|
||||||
maxLength,
|
|
||||||
minLength,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
placeholder,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
required,
|
required,
|
||||||
richTextComponentMap,
|
|
||||||
rtl,
|
|
||||||
style,
|
style,
|
||||||
user,
|
validate, // Users can pass in client side validation if they WANT to, but it's not required anymore
|
||||||
validate = richTextValidateHOC({ editorConfig }),
|
|
||||||
width,
|
width,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const memoizedValidate = useCallback(
|
const memoizedValidate = useCallback(
|
||||||
(value, validationOptions) => {
|
(value, validationOptions) => {
|
||||||
return validate(value, { ...validationOptions, props, required })
|
if (typeof validate === 'function') {
|
||||||
|
return validate(value, { ...validationOptions, props, required })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Important: do not add props to the dependencies array.
|
// Important: do not add props to the dependencies array.
|
||||||
// This would cause an infinite loop and endless re-rendering.
|
// This would cause an infinite loop and endless re-rendering.
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { FormState } from '@payloadcms/ui'
|
import type { SanitizedClientEditorConfig } from '@payloadcms/richtext-lexical'
|
||||||
import type { Block, Data, Field } from 'payload/types'
|
import type { FormFieldBase, FormState } from '@payloadcms/ui'
|
||||||
|
import type { Data, Field } from 'payload/types'
|
||||||
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { RenderFields } from '@payloadcms/ui'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Collapsible,
|
Collapsible,
|
||||||
ErrorPill,
|
ErrorPill,
|
||||||
Pill,
|
Pill,
|
||||||
RenderFields,
|
|
||||||
SectionTitle,
|
SectionTitle,
|
||||||
createNestedFieldPath,
|
createNestedFieldPath,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
@@ -19,6 +20,7 @@ import isDeepEqual from 'deep-equal'
|
|||||||
import { $getNodeByKey } from 'lexical'
|
import { $getNodeByKey } from 'lexical'
|
||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
|
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
|
||||||
import type { FieldProps } from '../../../../types'
|
import type { FieldProps } from '../../../../types'
|
||||||
import type { BlockFields, BlockNode } from '../nodes/BlocksNode'
|
import type { BlockFields, BlockNode } from '../nodes/BlocksNode'
|
||||||
|
|
||||||
@@ -26,11 +28,15 @@ import { FormSavePlugin } from './FormSavePlugin'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
baseClass: string
|
baseClass: string
|
||||||
block: Block
|
field: FormFieldBase & {
|
||||||
field: FieldProps
|
editorConfig: SanitizedClientEditorConfig // With rendered features n stuff
|
||||||
|
name: string
|
||||||
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
}
|
||||||
formData: BlockFields
|
formData: BlockFields
|
||||||
formSchema: Field[]
|
formSchema: Field[]
|
||||||
nodeKey: string
|
nodeKey: string
|
||||||
|
reducedBlock: ReducedBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,12 +47,13 @@ type Props = {
|
|||||||
export const BlockContent: React.FC<Props> = (props) => {
|
export const BlockContent: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
baseClass,
|
baseClass,
|
||||||
block: { labels },
|
|
||||||
field,
|
field,
|
||||||
formData,
|
formData,
|
||||||
formSchema,
|
formSchema,
|
||||||
nodeKey,
|
nodeKey,
|
||||||
|
reducedBlock: { labels },
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
// Used for saving collapsed to preferences (and gettin' it from there again)
|
// Used for saving collapsed to preferences (and gettin' it from there again)
|
||||||
@@ -92,10 +99,17 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
fullFieldsWithValues: FormState
|
fullFieldsWithValues: FormState
|
||||||
newFormData: Data
|
newFormData: Data
|
||||||
}) => {
|
}) => {
|
||||||
|
newFormData = {
|
||||||
|
...newFormData,
|
||||||
|
id: formData.id, // TODO: Why does form updatee not include theeeeem
|
||||||
|
blockName: formData.blockName, // TODO: Why does form updatee not include theeeeem
|
||||||
|
blockType: formData.blockType, // TODO: Why does form updatee not include theeeeem
|
||||||
|
}
|
||||||
|
|
||||||
// Recursively remove all undefined values from even being present in formData, as they will
|
// Recursively remove all undefined values from even being present in formData, as they will
|
||||||
// cause isDeepEqual to return false if, for example, formData has a key that fields.data
|
// cause isDeepEqual to return false if, for example, formData has a key that fields.data
|
||||||
// does not have, even if it's undefined.
|
// does not have, even if it's undefined.
|
||||||
// Currently, this happens if a block has another sub-blocks field. Inside of formData, that sub-blocks field has an undefined blockName property.
|
// Currently, this happens if a block has another sub-blocks field. Inside formData, that sub-blocks field has an undefined blockName property.
|
||||||
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
// Inside of fields.data however, that sub-blocks blockName property does not exist at all.
|
||||||
function removeUndefinedAndNullRecursively(obj: object) {
|
function removeUndefinedAndNullRecursively(obj: object) {
|
||||||
Object.keys(obj).forEach((key) => {
|
Object.keys(obj).forEach((key) => {
|
||||||
@@ -109,6 +123,8 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
removeUndefinedAndNullRecursively(newFormData)
|
removeUndefinedAndNullRecursively(newFormData)
|
||||||
removeUndefinedAndNullRecursively(formData)
|
removeUndefinedAndNullRecursively(formData)
|
||||||
|
|
||||||
|
console.log('before saving node data...', newFormData, 'old', formData)
|
||||||
|
|
||||||
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
|
||||||
// which would trigger the "Leave without saving" dialog unnecessarily
|
// which would trigger the "Leave without saving" dialog unnecessarily
|
||||||
if (!isDeepEqual(formData, newFormData)) {
|
if (!isDeepEqual(formData, newFormData)) {
|
||||||
@@ -120,6 +136,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const node: BlockNode = $getNodeByKey(nodeKey)
|
const node: BlockNode = $getNodeByKey(nodeKey)
|
||||||
if (node) {
|
if (node) {
|
||||||
|
console.log('saving node data...', newFormData)
|
||||||
node.setFields(newFormData as BlockFields)
|
node.setFields(newFormData as BlockFields)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -163,11 +180,6 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
}, [editor, nodeKey])
|
}, [editor, nodeKey])
|
||||||
|
|
||||||
const fieldSchemaWithPath = formSchema.map((field) => ({
|
|
||||||
...field,
|
|
||||||
path: createNestedFieldPath(null, field),
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
@@ -186,7 +198,7 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
: '[Singular Label]'}
|
: '[Singular Label]'}
|
||||||
</Pill>
|
</Pill>
|
||||||
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
|
||||||
{fieldHasErrors && <ErrorPill count={errorCount} withMessage />}
|
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
|
||||||
</div>
|
</div>
|
||||||
{editor.isEditable() && (
|
{editor.isEditable() && (
|
||||||
<Button
|
<Button
|
||||||
@@ -210,16 +222,12 @@ export const BlockContent: React.FC<Props> = (props) => {
|
|||||||
onCollapsedChange()
|
onCollapsedChange()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
[RenderFields]
|
<RenderFields
|
||||||
{/* <RenderFields
|
|
||||||
className={`${baseClass}__fields`}
|
className={`${baseClass}__fields`}
|
||||||
fieldSchema={fieldSchemaWithPath}
|
fieldMap={Array.isArray(formSchema) ? formSchema : []}
|
||||||
fieldTypes={field.fieldTypes}
|
|
||||||
forceRender
|
forceRender
|
||||||
margins="small"
|
margins="small"
|
||||||
permissions={field.permissions?.blocks?.[formData?.blockType]?.fields}
|
/>
|
||||||
readOnly={field.admin.readOnly}
|
|
||||||
/> */}
|
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<FormSavePlugin onChange={onFormChange} />
|
<FormSavePlugin onChange={onFormChange} />
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const FormSavePlugin: React.FC<Props> = (props) => {
|
|||||||
const newFormData = reduceFieldsToValues(fields, true)
|
const newFormData = reduceFieldsToValues(fields, true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('FormSavePlugin', newFormData)
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange({ fullFieldsWithValues: fields, newFormData })
|
onChange({ fullFieldsWithValues: fields, newFormData })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,133 +1,142 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {
|
import {
|
||||||
|
FieldPathProvider,
|
||||||
Form,
|
Form,
|
||||||
|
type FormProps,
|
||||||
|
type FormState,
|
||||||
buildInitialState,
|
buildInitialState,
|
||||||
buildStateFromSchema,
|
buildStateFromSchema,
|
||||||
|
getFormState,
|
||||||
useConfig,
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
|
useFieldPath,
|
||||||
useFormSubmitted,
|
useFormSubmitted,
|
||||||
useLocale,
|
useLocale,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import React, { useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { type BlockFields } from '../nodes/BlocksNode'
|
import { type BlockFields } from '../nodes/BlocksNode'
|
||||||
const baseClass = 'lexical-block'
|
const baseClass = 'lexical-block'
|
||||||
|
|
||||||
import type { Data } from 'payload/types'
|
import type { Data } from 'payload/types'
|
||||||
|
|
||||||
import { sanitizeFields } from 'payload/config'
|
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
|
||||||
|
import type { ClientComponentProps } from '../../types'
|
||||||
import type { BlocksFeatureProps } from '..'
|
import type { BlocksFeatureClientProps } from '../feature.client'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { transformInputFormSchema } from '../utils/transformInputFormSchema'
|
|
||||||
import { BlockContent } from './BlockContent'
|
import { BlockContent } from './BlockContent'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
blockFieldWrapperName: string
|
blockFieldWrapperName: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
/**
|
|
||||||
* This formData already comes wrapped in blockFieldWrapperName
|
|
||||||
*/
|
|
||||||
formData: BlockFields
|
formData: BlockFields
|
||||||
nodeKey?: string
|
nodeKey?: string
|
||||||
|
/**
|
||||||
|
* This transformedFormData already comes wrapped in blockFieldWrapperName
|
||||||
|
*/
|
||||||
|
transformedFormData: BlockFields
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockComponent: React.FC<Props> = (props) => {
|
export const BlockComponent: React.FC<Props> = (props) => {
|
||||||
const { blockFieldWrapperName, formData, nodeKey } = props
|
const { blockFieldWrapperName, formData, nodeKey } = props
|
||||||
const payloadConfig = useConfig()
|
const config = useConfig()
|
||||||
const submitted = useFormSubmitted()
|
const submitted = useFormSubmitted()
|
||||||
|
const { id } = useDocumentInfo()
|
||||||
|
const { schemaPath } = useFieldPath()
|
||||||
const { editorConfig, field: parentLexicalRichTextField } = useEditorConfigContext()
|
const { editorConfig, field: parentLexicalRichTextField } = useEditorConfigContext()
|
||||||
|
|
||||||
const block = (
|
const [initialState, setInitialState] = useState<FormState | false>(false)
|
||||||
editorConfig?.resolvedFeatureMap?.get('blocks')?.props as BlocksFeatureProps
|
const {
|
||||||
)?.blocks?.find((block) => block.slug === formData?.blockType)
|
field: { richTextComponentMap },
|
||||||
|
} = useEditorConfigContext()
|
||||||
|
|
||||||
const unsanitizedFormSchema = block?.fields || []
|
console.log('1. Loading node data', formData)
|
||||||
|
|
||||||
|
const componentMapRenderedFieldsPath = `feature.blocks.fields.${formData?.blockType}`
|
||||||
|
const schemaFieldsPath = `${schemaPath}.feature.blocks.${formData?.blockType}`
|
||||||
|
|
||||||
|
const reducedBlock: ReducedBlock = (
|
||||||
|
editorConfig?.resolvedFeatureMap?.get('blocks')
|
||||||
|
?.clientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||||
|
)?.reducedBlocks?.find((block) => block.slug === formData?.blockType)
|
||||||
|
|
||||||
|
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) // Field Schema
|
||||||
|
useEffect(() => {
|
||||||
|
const awaitInitialState = async () => {
|
||||||
|
const state = await getFormState({
|
||||||
|
apiRoute: config.routes.api,
|
||||||
|
body: {
|
||||||
|
id,
|
||||||
|
data: JSON.parse(JSON.stringify(formData)),
|
||||||
|
operation: 'update',
|
||||||
|
schemaPath: schemaFieldsPath,
|
||||||
|
},
|
||||||
|
serverURL: config.serverURL,
|
||||||
|
}) // Form State
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
setInitialState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData) {
|
||||||
|
void awaitInitialState()
|
||||||
|
}
|
||||||
|
}, [config.routes.api, config.serverURL, schemaFieldsPath, id])
|
||||||
|
|
||||||
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
|
async ({ formState: prevFormState }) => {
|
||||||
|
return await getFormState({
|
||||||
|
apiRoute: config.routes.api,
|
||||||
|
body: {
|
||||||
|
id,
|
||||||
|
formState: prevFormState,
|
||||||
|
operation: 'update',
|
||||||
|
schemaPath: schemaFieldsPath,
|
||||||
|
},
|
||||||
|
serverURL: config.serverURL,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
[config.routes.api, config.serverURL, schemaFieldsPath, id],
|
||||||
|
)
|
||||||
|
|
||||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
//const formSchema = transformInputFormSchema(fieldMap, blockFieldWrapperName)
|
||||||
const formSchema = transformInputFormSchema(
|
|
||||||
sanitizeFields({
|
|
||||||
// @ts-expect-error-next-line TODO: Fix this
|
|
||||||
config: payloadConfig,
|
|
||||||
fields: unsanitizedFormSchema,
|
|
||||||
validRelationships,
|
|
||||||
}),
|
|
||||||
blockFieldWrapperName,
|
|
||||||
)
|
|
||||||
|
|
||||||
const initialStateRef = React.useRef<Data>(null) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
const initialStateRef = React.useRef<Data>(null) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
|
||||||
|
|
||||||
const config = useConfig()
|
console.log('Bloocks initialState', initialState)
|
||||||
const { t } = useTranslation()
|
|
||||||
const { code: locale } = useLocale()
|
|
||||||
const { getDocPreferences } = useDocumentInfo()
|
|
||||||
|
|
||||||
// initialState State
|
|
||||||
const [initialState, setInitialState] = React.useState<Data>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function createInitialState() {
|
|
||||||
const preferences = await getDocPreferences()
|
|
||||||
|
|
||||||
const stateFromSchema = await buildStateFromSchema({
|
|
||||||
// @ts-expect-error-next-line TODO: Fix this
|
|
||||||
config,
|
|
||||||
data: JSON.parse(JSON.stringify(formData)),
|
|
||||||
fieldSchema: formSchema as any,
|
|
||||||
locale,
|
|
||||||
operation: 'create',
|
|
||||||
preferences,
|
|
||||||
t,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!initialStateRef.current) {
|
|
||||||
// @ts-expect-error-next-line TODO: Fix this
|
|
||||||
initialStateRef.current = buildInitialState(JSON.parse(JSON.stringify(formData)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have to merge the output of buildInitialState (above this useEffect) with the output of buildStateFromSchema.
|
|
||||||
// That's because the output of buildInitialState provides important properties necessary for THIS block,
|
|
||||||
// like blockName, blockType and id, while buildStateFromSchema provides the correct output of this block's data,
|
|
||||||
// e.g. if this block has a sub-block (like the `rows` property)
|
|
||||||
const consolidatedInitialState = {
|
|
||||||
...initialStateRef.current,
|
|
||||||
...stateFromSchema,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to delete consolidatedInitialState[blockFieldWrapperName] - it's an unnecessary property.
|
|
||||||
// It causes issues when we later use reduceFieldsToValues in the FormSavePlugin, because that may
|
|
||||||
// cause some sub-fields to "use" the wrong value from the blockFieldWrapperName property (which shouldn't be there)
|
|
||||||
// This fixes the 'should respect row removal in nested array field' fields lexical e2e test
|
|
||||||
delete consolidatedInitialState[blockFieldWrapperName]
|
|
||||||
|
|
||||||
setInitialState(consolidatedInitialState)
|
|
||||||
}
|
|
||||||
void createInitialState()
|
|
||||||
}, [setInitialState, config, locale, getDocPreferences, t]) // do not add formData or formSchema here, it causes an endless loop
|
|
||||||
|
|
||||||
// Memoized Form JSX
|
// Memoized Form JSX
|
||||||
const formContent = useMemo(() => {
|
const formContent = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
block &&
|
reducedBlock &&
|
||||||
initialState && (
|
initialState && (
|
||||||
<Form fields={formSchema} initialState={initialState} submitted={submitted}>
|
<FieldPathProvider path="" schemaPath="">
|
||||||
<BlockContent
|
<Form
|
||||||
baseClass={baseClass}
|
fields={fieldMap}
|
||||||
block={block}
|
initialState={initialState}
|
||||||
field={parentLexicalRichTextField}
|
onChange={[onChange]}
|
||||||
formData={formData}
|
submitted={submitted}
|
||||||
formSchema={formSchema}
|
>
|
||||||
nodeKey={nodeKey}
|
<BlockContent
|
||||||
/>
|
baseClass={baseClass}
|
||||||
</Form>
|
field={parentLexicalRichTextField}
|
||||||
|
formData={formData}
|
||||||
|
formSchema={fieldMap}
|
||||||
|
nodeKey={nodeKey}
|
||||||
|
reducedBlock={reducedBlock}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</FieldPathProvider>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, [block, parentLexicalRichTextField, nodeKey, submitted, initialState])
|
}, [fieldMap, parentLexicalRichTextField, nodeKey, submitted, initialState, reducedBlock])
|
||||||
|
|
||||||
return <div className={baseClass}>{formContent}</div>
|
return <div className={baseClass}>{formContent}</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { BlocksFeatureProps } from '..'
|
import type { ClientComponentProps } from '../../types'
|
||||||
|
import type { BlocksFeatureClientProps } from '../feature.client'
|
||||||
|
|
||||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||||
import { $createBlockNode } from '../nodes/BlocksNode'
|
import { $createBlockNode } from '../nodes/BlocksNode'
|
||||||
@@ -35,7 +36,7 @@ const insertBlock = ({
|
|||||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||||
id: null,
|
id: null,
|
||||||
blockName: '',
|
blockName: '',
|
||||||
blockType: blockType,
|
blockType,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
@@ -45,7 +46,7 @@ const insertBlock = ({
|
|||||||
$createBlockNode({
|
$createBlockNode({
|
||||||
id: null,
|
id: null,
|
||||||
blockName: '',
|
blockName: '',
|
||||||
blockType: blockType,
|
blockType,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -68,9 +69,9 @@ export const BlocksDrawerComponent: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowIndex: number, blockType: string) => {
|
(rowIndex: number, blockType: string) => {
|
||||||
insertBlock({
|
insertBlock({
|
||||||
blockType: blockType,
|
blockType,
|
||||||
editor,
|
editor,
|
||||||
replaceNodeKey,
|
replaceNodeKey,
|
||||||
})
|
})
|
||||||
@@ -83,8 +84,10 @@ export const BlocksDrawerComponent: React.FC = () => {
|
|||||||
depth: editDepth,
|
depth: editDepth,
|
||||||
})
|
})
|
||||||
|
|
||||||
const blocks = (editorConfig?.resolvedFeatureMap?.get('blocks')?.props as BlocksFeatureProps)
|
const reducedBlocks = (
|
||||||
?.blocks
|
editorConfig?.resolvedFeatureMap?.get('blocks')
|
||||||
|
?.clientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
|
||||||
|
)?.reducedBlocks
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerCommand<{
|
return editor.registerCommand<{
|
||||||
@@ -104,7 +107,7 @@ export const BlocksDrawerComponent: React.FC = () => {
|
|||||||
<BlocksDrawer
|
<BlocksDrawer
|
||||||
addRow={addRow}
|
addRow={addRow}
|
||||||
addRowIndex={0}
|
addRowIndex={0}
|
||||||
blocks={blocks}
|
blocks={reducedBlocks}
|
||||||
drawerSlug={drawerSlug}
|
drawerSlug={drawerSlug}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
|
||||||
|
import type { ReducedBlock } from '../../../../../ui/src/utilities/buildComponentMap/types'
|
||||||
|
import type { FeatureProviderProviderClient } from '../types'
|
||||||
|
|
||||||
|
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||||
|
import { BlockIcon } from '../../lexical/ui/icons/Block'
|
||||||
|
import { createClientComponent } from '../createClientComponent'
|
||||||
|
import { BlockNode } from './nodes/BlocksNode'
|
||||||
|
import { BlocksPlugin } from './plugin'
|
||||||
|
import { INSERT_BLOCK_COMMAND } from './plugin/commands'
|
||||||
|
|
||||||
|
export type BlocksFeatureClientProps = {
|
||||||
|
reducedBlocks: ReducedBlock[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlocksFeatureClient: FeatureProviderProviderClient<BlocksFeatureClientProps> = (props) => {
|
||||||
|
return {
|
||||||
|
clientFeatureProps: props,
|
||||||
|
feature: () => ({
|
||||||
|
clientFeatureProps: props,
|
||||||
|
nodes: [BlockNode],
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
Component: BlocksPlugin,
|
||||||
|
position: 'normal',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
slashMenu: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Blocks',
|
||||||
|
key: 'blocks',
|
||||||
|
options: [
|
||||||
|
...props.reducedBlocks.map((block) => {
|
||||||
|
return new SlashMenuOption('block-' + block.slug, {
|
||||||
|
Icon: BlockIcon,
|
||||||
|
displayName: ({ i18n }) => {
|
||||||
|
if (!block.labels.singular) {
|
||||||
|
return block.slug
|
||||||
|
}
|
||||||
|
|
||||||
|
return getTranslation(block.labels.singular, i18n)
|
||||||
|
},
|
||||||
|
keywords: ['block', 'blocks', block.slug],
|
||||||
|
onSelect: ({ editor }) => {
|
||||||
|
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
||||||
|
id: null,
|
||||||
|
blockName: '',
|
||||||
|
blockType: block.slug,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlocksFeatureClientComponent = createClientComponent(BlocksFeatureClient)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import type { Block, BlockField, Field } from 'payload/types'
|
||||||
|
|
||||||
|
import { baseBlockFields, sanitizeFields } from 'payload/config'
|
||||||
|
import { fieldsToJSONSchema, formatLabels } from 'payload/utilities'
|
||||||
|
|
||||||
|
import type { FeatureProviderProviderServer } from '../types'
|
||||||
|
import type { BlocksFeatureClientProps } from './feature.client'
|
||||||
|
|
||||||
|
import { BlocksFeatureClientComponent } from './feature.client'
|
||||||
|
import { BlockNode } from './nodes/BlocksNode'
|
||||||
|
import { blockPopulationPromiseHOC } from './populationPromise'
|
||||||
|
import { blockValidationHOC } from './validate'
|
||||||
|
|
||||||
|
export type BlocksFeatureProps = {
|
||||||
|
blocks: Block[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlocksFeature: FeatureProviderProviderServer<
|
||||||
|
BlocksFeatureProps,
|
||||||
|
BlocksFeatureClientProps
|
||||||
|
> = (props) => {
|
||||||
|
// Sanitization taken from payload/src/fields/config/sanitize.ts
|
||||||
|
|
||||||
|
if (props?.blocks?.length) {
|
||||||
|
props.blocks = props.blocks.map((block) => {
|
||||||
|
return {
|
||||||
|
...block,
|
||||||
|
fields: block.fields.concat(baseBlockFields),
|
||||||
|
labels: !block.labels ? formatLabels(block.slug) : block.labels,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// unSanitizedBlock.fields are sanitized in the React component and not here.
|
||||||
|
// That's because we do not have access to the payload config here.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build clientProps
|
||||||
|
const clientProps: BlocksFeatureClientProps = {
|
||||||
|
reducedBlocks: [],
|
||||||
|
}
|
||||||
|
for (const block of props.blocks) {
|
||||||
|
clientProps.reducedBlocks.push({
|
||||||
|
slug: block.slug,
|
||||||
|
imageAltText: block.imageAltText,
|
||||||
|
imageURL: block.imageURL,
|
||||||
|
labels: block.labels,
|
||||||
|
subfields: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
feature: () => {
|
||||||
|
return {
|
||||||
|
ClientComponent: BlocksFeatureClientComponent,
|
||||||
|
clientFeatureProps: clientProps,
|
||||||
|
generateSchemaMap: ({ config, props }) => {
|
||||||
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
|
|
||||||
|
const schemaMap: {
|
||||||
|
[key: string]: Field[]
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
for (const block of props.blocks) {
|
||||||
|
const unSanitizedFormSchema = block.fields || []
|
||||||
|
|
||||||
|
const formSchema = sanitizeFields({
|
||||||
|
config,
|
||||||
|
fields: unSanitizedFormSchema,
|
||||||
|
validRelationships,
|
||||||
|
})
|
||||||
|
|
||||||
|
schemaMap[block.slug] = formSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaMap
|
||||||
|
},
|
||||||
|
generatedTypes: {
|
||||||
|
modifyOutputSchema: ({ currentSchema, field, interfaceNameDefinitions }) => {
|
||||||
|
const blocksField: BlockField = {
|
||||||
|
name: field?.name + '_lexical_blocks',
|
||||||
|
type: 'blocks',
|
||||||
|
blocks: props.blocks,
|
||||||
|
}
|
||||||
|
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
|
||||||
|
// we don't actually use the JSON Schema itself in the generated types yet.
|
||||||
|
fieldsToJSONSchema({}, [blocksField], interfaceNameDefinitions)
|
||||||
|
|
||||||
|
return currentSchema
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
node: BlockNode,
|
||||||
|
populationPromises: [blockPopulationPromiseHOC(props)],
|
||||||
|
validations: [blockValidationHOC(props)],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
serverFeatureProps: props,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key: 'blocks',
|
||||||
|
serverFeatureProps: props,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import type { Block, BlockField } from 'payload/types'
|
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
|
||||||
import { baseBlockFields } from 'payload/config'
|
|
||||||
import { fieldsToJSONSchema, formatLabels } from 'payload/utilities'
|
|
||||||
|
|
||||||
import type { FeatureProvider } from '../types'
|
|
||||||
|
|
||||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
|
||||||
import { BlockNode } from './nodes/BlocksNode'
|
|
||||||
import { INSERT_BLOCK_COMMAND } from './plugin/commands'
|
|
||||||
import { blockPopulationPromiseHOC } from './populationPromise'
|
|
||||||
import { blockValidationHOC } from './validate'
|
|
||||||
|
|
||||||
export type BlocksFeatureProps = {
|
|
||||||
blocks: Block[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BlocksFeature = (props?: BlocksFeatureProps): FeatureProvider => {
|
|
||||||
// Sanitization taken from payload/src/fields/config/sanitize.ts
|
|
||||||
if (props?.blocks?.length) {
|
|
||||||
props.blocks = props.blocks.map((block) => {
|
|
||||||
return {
|
|
||||||
...block,
|
|
||||||
fields: block.fields.concat(baseBlockFields),
|
|
||||||
labels: !block.labels ? formatLabels(block.slug) : block.labels,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// unsanitizedBlock.fields are sanitized in the React component and not here.
|
|
||||||
// That's because we do not have access to the payload config here.
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
feature: () => {
|
|
||||||
return {
|
|
||||||
generatedTypes: {
|
|
||||||
modifyOutputSchema: ({ currentSchema, field, interfaceNameDefinitions }) => {
|
|
||||||
const blocksField: BlockField = {
|
|
||||||
name: field?.name + '_lexical_blocks',
|
|
||||||
type: 'blocks',
|
|
||||||
blocks: props.blocks,
|
|
||||||
}
|
|
||||||
// This is only done so that interfaceNameDefinitions sets those block's interfaceNames.
|
|
||||||
// we don't actually use the JSON Schema itself in the generated types yet.
|
|
||||||
fieldsToJSONSchema({}, [blocksField], interfaceNameDefinitions)
|
|
||||||
|
|
||||||
return currentSchema
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
type: BlockNode.getType(),
|
|
||||||
node: BlockNode,
|
|
||||||
populationPromises: [blockPopulationPromiseHOC(props)],
|
|
||||||
validations: [blockValidationHOC(props)],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
Component: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('./plugin').then((module) => module.BlocksPlugin),
|
|
||||||
position: 'normal',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
props: props,
|
|
||||||
slashMenu: {
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
displayName: 'Blocks',
|
|
||||||
key: 'blocks',
|
|
||||||
options: [
|
|
||||||
...props.blocks.map((block) => {
|
|
||||||
return new SlashMenuOption('block-' + block.slug, {
|
|
||||||
Icon: () =>
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
import('../../lexical/ui/icons/Block').then((module) => module.BlockIcon),
|
|
||||||
displayName: ({ i18n }) => {
|
|
||||||
// TODO: fix this
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
return getTranslation(block.labels.singular, i18n)
|
|
||||||
},
|
|
||||||
keywords: ['block', 'blocks', block.slug],
|
|
||||||
onSelect: ({ editor }) => {
|
|
||||||
editor.dispatchCommand(INSERT_BLOCK_COMMAND, {
|
|
||||||
id: null,
|
|
||||||
blockName: '',
|
|
||||||
blockType: block.slug,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key: 'blocks',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -96,8 +96,9 @@ export class BlockNode extends DecoratorBlockNode {
|
|||||||
return (
|
return (
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
blockFieldWrapperName={blockFieldWrapperName}
|
blockFieldWrapperName={blockFieldWrapperName}
|
||||||
formData={transformedFormData}
|
formData={this.getFields()}
|
||||||
nodeKey={this.getKey()}
|
nodeKey={this.getKey()}
|
||||||
|
transformedFormData={transformedFormData}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { INSERT_BLOCK_COMMAND } from './commands'
|
|||||||
|
|
||||||
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
|
export type InsertBlockPayload = Exclude<BlockFields, 'id'>
|
||||||
|
|
||||||
export function BlocksPlugin(): JSX.Element | null {
|
export function BlocksPlugin(): React.ReactNode {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import type { Block } from 'payload/types'
|
|||||||
|
|
||||||
import { sanitizeFields } from 'payload/config'
|
import { sanitizeFields } from 'payload/config'
|
||||||
|
|
||||||
import type { BlocksFeatureProps } from '.'
|
|
||||||
import type { PopulationPromise } from '../types'
|
import type { PopulationPromise } from '../types'
|
||||||
|
import type { BlocksFeatureProps } from './feature.server'
|
||||||
import type { SerializedBlockNode } from './nodes/BlocksNode'
|
import type { SerializedBlockNode } from './nodes/BlocksNode'
|
||||||
|
|
||||||
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
import { recurseNestedFields } from '../../../populate/recurseNestedFields'
|
||||||
@@ -61,7 +61,7 @@ export const blockPopulationPromiseHOC = (
|
|||||||
promises,
|
promises,
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
// The afterReadPromise gets its data from looking for field.name inside of the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
|
// The afterReadPromise gets its data from looking for field.name inside the siblingDoc. Thus, here we cannot pass the whole document's siblingDoc, but only the siblingDoc (sibling fields) of the current field.
|
||||||
siblingDoc: blockFieldData,
|
siblingDoc: blockFieldData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
|
import type { BlocksFeatureProps } from '@payloadcms/richtext-lexical'
|
||||||
import type { Block } from 'payload/types'
|
import type { Block } from 'payload/types'
|
||||||
|
|
||||||
import { sanitizeFields } from 'payload/config'
|
import { sanitizeFields } from 'payload/config'
|
||||||
|
|
||||||
import type { BlocksFeatureProps } from '.'
|
|
||||||
import type { NodeValidation } from '../types'
|
import type { NodeValidation } from '../types'
|
||||||
import type { SerializedBlockNode } from './nodes/BlocksNode'
|
import type { SerializedBlockNode } from './nodes/BlocksNode'
|
||||||
|
|
||||||
export const blockValidationHOC = (
|
export const blockValidationHOC = (
|
||||||
props: BlocksFeatureProps,
|
props: BlocksFeatureProps,
|
||||||
): NodeValidation<SerializedBlockNode> => {
|
): NodeValidation<SerializedBlockNode> => {
|
||||||
const blockValidation: NodeValidation<SerializedBlockNode> = async ({
|
const blockValidation: NodeValidation<SerializedBlockNode> = async ({ node, validation }) => {
|
||||||
node,
|
|
||||||
payloadConfig,
|
|
||||||
validation,
|
|
||||||
}) => {
|
|
||||||
const blockFieldData = node.fields
|
const blockFieldData = node.fields
|
||||||
const blocks: Block[] = props.blocks
|
const blocks: Block[] = props.blocks
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: { req },
|
options: {
|
||||||
|
req,
|
||||||
|
req: {
|
||||||
|
payload: { config },
|
||||||
|
},
|
||||||
|
},
|
||||||
} = validation
|
} = validation
|
||||||
|
|
||||||
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
|
||||||
const validRelationships = payloadConfig.collections.map((c) => c.slug) || []
|
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
block.fields = sanitizeFields({
|
block.fields = sanitizeFields({
|
||||||
config: payloadConfig,
|
config,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
})
|
})
|
||||||
@@ -57,13 +58,10 @@ export const blockValidationHOC = (
|
|||||||
const validationResult = await field.validate(fieldValue, {
|
const validationResult = await field.validate(fieldValue, {
|
||||||
...field,
|
...field,
|
||||||
id: validation.options.id,
|
id: validation.options.id,
|
||||||
config: payloadConfig,
|
|
||||||
data: fieldValue,
|
data: fieldValue,
|
||||||
operation: validation.options.operation,
|
operation: validation.options.operation,
|
||||||
payload: validation.options.payload,
|
req,
|
||||||
siblingData: validation.options.siblingData,
|
siblingData: validation.options.siblingData,
|
||||||
t: validation.options.t,
|
|
||||||
user: validation.options.user,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (validationResult !== true) {
|
if (validationResult !== true) {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
|||||||
const fieldSchema = sanitizeFields({
|
const fieldSchema = sanitizeFields({
|
||||||
// TODO: fix this
|
// TODO: fix this
|
||||||
// @ts-expect-error-next-line
|
// @ts-expect-error-next-line
|
||||||
config: config,
|
config,
|
||||||
fields: fieldSchemaUnsanitized,
|
fields: fieldSchemaUnsanitized,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
})
|
})
|
||||||
@@ -94,7 +94,7 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
|||||||
const fieldSchema = sanitizeFields({
|
const fieldSchema = sanitizeFields({
|
||||||
// TODO: fix this
|
// TODO: fix this
|
||||||
// @ts-expect-error-next-line
|
// @ts-expect-error-next-line
|
||||||
config: config,
|
config,
|
||||||
fields: fieldSchemaUnsanitized,
|
fields: fieldSchemaUnsanitized,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
})
|
})
|
||||||
@@ -102,16 +102,12 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
|||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const preferences = await getDocPreferences()
|
const preferences = await getDocPreferences()
|
||||||
const state = await buildStateFromSchema({
|
const state = await buildStateFromSchema({
|
||||||
// TODO: fix this
|
|
||||||
// @ts-expect-error-next-line
|
|
||||||
config,
|
|
||||||
data: deepCopyObject(fields || {}),
|
data: deepCopyObject(fields || {}),
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
locale,
|
locale,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
preferences,
|
preferences,
|
||||||
t,
|
req,
|
||||||
user,
|
|
||||||
})
|
})
|
||||||
setInitialState(state)
|
setInitialState(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client'
|
||||||
import type { LexicalCommand } from 'lexical'
|
import type { LexicalCommand } from 'lexical'
|
||||||
|
|
||||||
import { createCommand } from 'lexical'
|
import { createCommand } from 'lexical'
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export const BlockQuoteFeature: FeatureProviderProviderServer<undefined, undefin
|
|||||||
markdownTransformers: [MarkdownTransformer],
|
markdownTransformers: [MarkdownTransformer],
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
type: QuoteNode.getType(),
|
|
||||||
converters: {
|
converters: {
|
||||||
html: {
|
html: {
|
||||||
converter: async ({ converters, node, parent }) => {
|
converter: async ({ converters, node, parent }) => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const baseClass = 'lexical-link-edit-drawer'
|
|||||||
|
|
||||||
export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, stateData }) => {
|
export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, stateData }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { id, getDocPreferences } = useDocumentInfo()
|
const { id } = useDocumentInfo()
|
||||||
const { schemaPath } = useFieldPath()
|
const { schemaPath } = useFieldPath()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const [initialState, setInitialState] = useState<FormState | false>(false)
|
const [initialState, setInitialState] = useState<FormState | false>(false)
|
||||||
@@ -37,14 +37,11 @@ export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, sta
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const awaitInitialState = async () => {
|
const awaitInitialState = async () => {
|
||||||
const docPreferences = await getDocPreferences()
|
|
||||||
|
|
||||||
const state = await getFormState({
|
const state = await getFormState({
|
||||||
apiRoute: config.routes.api,
|
apiRoute: config.routes.api,
|
||||||
body: {
|
body: {
|
||||||
id,
|
id,
|
||||||
data: stateData,
|
data: stateData,
|
||||||
docPreferences,
|
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
schemaPath: schemaFieldsPath,
|
schemaPath: schemaFieldsPath,
|
||||||
},
|
},
|
||||||
@@ -57,17 +54,14 @@ export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, sta
|
|||||||
if (stateData) {
|
if (stateData) {
|
||||||
void awaitInitialState()
|
void awaitInitialState()
|
||||||
}
|
}
|
||||||
}, [config.routes.api, config.serverURL, schemaFieldsPath, getDocPreferences, id, stateData])
|
}, [config.routes.api, config.serverURL, schemaFieldsPath, id, stateData])
|
||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
const docPreferences = await getDocPreferences()
|
return await getFormState({
|
||||||
|
|
||||||
return getFormState({
|
|
||||||
apiRoute: config.routes.api,
|
apiRoute: config.routes.api,
|
||||||
body: {
|
body: {
|
||||||
id,
|
id,
|
||||||
docPreferences,
|
|
||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
operation: 'update',
|
operation: 'update',
|
||||||
schemaPath: schemaFieldsPath,
|
schemaPath: schemaFieldsPath,
|
||||||
@@ -76,7 +70,7 @@ export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, sta
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
[config.routes.api, config.serverURL, schemaFieldsPath, getDocPreferences, id],
|
[config.routes.api, config.serverURL, schemaFieldsPath, id],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,11 +83,7 @@ export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, sta
|
|||||||
onChange={[onChange]}
|
onChange={[onChange]}
|
||||||
onSubmit={handleModalSubmit}
|
onSubmit={handleModalSubmit}
|
||||||
>
|
>
|
||||||
<RenderFields
|
<RenderFields fieldMap={Array.isArray(fieldMap) ? fieldMap : []} forceRender />
|
||||||
fieldMap={Array.isArray(fieldMap) ? fieldMap : []}
|
|
||||||
forceRender
|
|
||||||
readOnly={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormSubmit>{t('general:submit')}</FormSubmit>
|
<FormSubmit>{t('general:submit')}</FormSubmit>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ export type LinkFeatureServerProps = ExclusiveLinkCollectionsProps & {
|
|||||||
fields?:
|
fields?:
|
||||||
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
| ((args: { config: SanitizedConfig; defaultFields: Field[]; i18n: I18n }) => Field[])
|
||||||
| Field[]
|
| Field[]
|
||||||
|
|
||||||
//someFunction: (something: number) => string
|
|
||||||
someFunction?: React.FC
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps, ClientProps> = (
|
export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps, ClientProps> = (
|
||||||
@@ -64,11 +61,6 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
|||||||
disabledCollections: props.disabledCollections,
|
disabledCollections: props.disabledCollections,
|
||||||
enabledCollections: props.enabledCollections,
|
enabledCollections: props.enabledCollections,
|
||||||
} as ExclusiveLinkCollectionsProps,
|
} as ExclusiveLinkCollectionsProps,
|
||||||
generateComponentMap: () => {
|
|
||||||
return {
|
|
||||||
someFunction: props.someFunction,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
generateSchemaMap: ({ config, props, schemaMap, schemaPath }) => {
|
generateSchemaMap: ({ config, props, schemaMap, schemaPath }) => {
|
||||||
const i18n = initI18n({ config: config.i18n, context: 'client', translations })
|
const i18n = initI18n({ config: config.i18n, context: 'client', translations })
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,12 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
|
|||||||
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
||||||
node,
|
node,
|
||||||
nodeValidations,
|
nodeValidations,
|
||||||
payloadConfig,
|
|
||||||
validation,
|
validation,
|
||||||
}: {
|
}: {
|
||||||
node: T
|
node: T
|
||||||
nodeValidations: Map<string, Array<NodeValidation>>
|
nodeValidations: Map<string, Array<NodeValidation>>
|
||||||
payloadConfig: SanitizedConfig
|
|
||||||
validation: {
|
validation: {
|
||||||
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
|
options: ValidateOptions<unknown, unknown, RichTextField>
|
||||||
value: SerializedEditorState
|
value: SerializedEditorState
|
||||||
}
|
}
|
||||||
}) => Promise<string | true> | string | true
|
}) => Promise<string | true> | string | true
|
||||||
|
|||||||
@@ -232,17 +232,8 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { BlocksFeature } from './field/features/Blocks'
|
|
||||||
export {
|
|
||||||
$createBlockNode,
|
|
||||||
$isBlockNode,
|
|
||||||
type BlockFields,
|
|
||||||
BlockNode,
|
|
||||||
type SerializedBlockNode,
|
|
||||||
} from './field/features/Blocks/nodes/BlocksNode'
|
|
||||||
export { HeadingFeature } from './field/features/Heading'
|
export { HeadingFeature } from './field/features/Heading'
|
||||||
export { ParagraphFeature } from './field/features/Paragraph'
|
export { ParagraphFeature } from './field/features/Paragraph'
|
||||||
|
|
||||||
export { RelationshipFeature } from './field/features/Relationship'
|
export { RelationshipFeature } from './field/features/Relationship'
|
||||||
export {
|
export {
|
||||||
$createRelationshipNode,
|
$createRelationshipNode,
|
||||||
@@ -251,10 +242,11 @@ export {
|
|||||||
RelationshipNode,
|
RelationshipNode,
|
||||||
type SerializedRelationshipNode,
|
type SerializedRelationshipNode,
|
||||||
} from './field/features/Relationship/nodes/RelationshipNode'
|
} from './field/features/Relationship/nodes/RelationshipNode'
|
||||||
export { UploadFeature } from './field/features/Upload'
|
|
||||||
|
|
||||||
|
export { UploadFeature } from './field/features/Upload'
|
||||||
export type { UploadFeatureProps } from './field/features/Upload'
|
export type { UploadFeatureProps } from './field/features/Upload'
|
||||||
export type { RawUploadPayload } from './field/features/Upload/nodes/UploadNode'
|
export type { RawUploadPayload } from './field/features/Upload/nodes/UploadNode'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
$createUploadNode,
|
$createUploadNode,
|
||||||
$isUploadNode,
|
$isUploadNode,
|
||||||
@@ -264,6 +256,14 @@ export {
|
|||||||
} from './field/features/Upload/nodes/UploadNode'
|
} from './field/features/Upload/nodes/UploadNode'
|
||||||
export { AlignFeature } from './field/features/align/feature.server'
|
export { AlignFeature } from './field/features/align/feature.server'
|
||||||
export { BlockQuoteFeature } from './field/features/blockquote/feature.server'
|
export { BlockQuoteFeature } from './field/features/blockquote/feature.server'
|
||||||
|
export { BlocksFeature, type BlocksFeatureProps } from './field/features/blocks/feature.server'
|
||||||
|
export {
|
||||||
|
$createBlockNode,
|
||||||
|
$isBlockNode,
|
||||||
|
type BlockFields,
|
||||||
|
BlockNode,
|
||||||
|
type SerializedBlockNode,
|
||||||
|
} from './field/features/blocks/nodes/BlocksNode'
|
||||||
export { TextDropdownSectionWithEntries } from './field/features/common/floatingSelectToolbarTextDropdownSection'
|
export { TextDropdownSectionWithEntries } from './field/features/common/floatingSelectToolbarTextDropdownSection'
|
||||||
export {
|
export {
|
||||||
HTMLConverterFeature,
|
HTMLConverterFeature,
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
|
import type { SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical'
|
||||||
import type { SerializedEditorState } from 'lexical'
|
import type { SerializedEditorState } from 'lexical'
|
||||||
import type { RichTextField, Validate } from 'payload/types'
|
import type { RichTextField, Validate } from 'payload/types'
|
||||||
|
|
||||||
import type { SanitizedEditorConfig } from '../field/lexical/config/types'
|
|
||||||
|
|
||||||
import { defaultRichTextValue, defaultRichTextValueV2 } from '../populate/defaultValue'
|
import { defaultRichTextValue, defaultRichTextValueV2 } from '../populate/defaultValue'
|
||||||
import { validateNodes } from './validateNodes'
|
import { validateNodes } from './validateNodes'
|
||||||
|
|
||||||
export const richTextValidateHOC = ({ editorConfig }: { editorConfig: SanitizedEditorConfig }) => {
|
export const richTextValidateHOC = ({
|
||||||
const richTextValidate: Validate<
|
editorConfig,
|
||||||
SerializedEditorState,
|
}: {
|
||||||
SerializedEditorState,
|
editorConfig: SanitizedServerEditorConfig
|
||||||
unknown,
|
}) => {
|
||||||
RichTextField
|
const richTextValidate: Validate<SerializedEditorState, unknown, unknown, RichTextField> = async (
|
||||||
> = async (value, options) => {
|
value,
|
||||||
const { required, t } = options
|
options,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
req: { t },
|
||||||
|
required,
|
||||||
|
} = options
|
||||||
|
|
||||||
if (required) {
|
if (required) {
|
||||||
if (
|
if (
|
||||||
@@ -35,7 +39,6 @@ export const richTextValidateHOC = ({ editorConfig }: { editorConfig: SanitizedE
|
|||||||
return await validateNodes({
|
return await validateNodes({
|
||||||
nodeValidations: editorConfig.features.validations,
|
nodeValidations: editorConfig.features.validations,
|
||||||
nodes: rootNodes,
|
nodes: rootNodes,
|
||||||
payloadConfig: options.config,
|
|
||||||
validation: {
|
validation: {
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
|
||||||
import type { SanitizedConfig } from 'payload/config'
|
|
||||||
import type { RichTextField, ValidateOptions } from 'payload/types'
|
import type { RichTextField, ValidateOptions } from 'payload/types'
|
||||||
|
|
||||||
import type { NodeValidation } from '../field/features/types'
|
import type { NodeValidation } from '../field/features/types'
|
||||||
@@ -7,14 +6,12 @@ import type { NodeValidation } from '../field/features/types'
|
|||||||
export async function validateNodes({
|
export async function validateNodes({
|
||||||
nodeValidations,
|
nodeValidations,
|
||||||
nodes,
|
nodes,
|
||||||
payloadConfig,
|
|
||||||
validation: validationFromProps,
|
validation: validationFromProps,
|
||||||
}: {
|
}: {
|
||||||
nodeValidations: Map<string, Array<NodeValidation>>
|
nodeValidations: Map<string, Array<NodeValidation>>
|
||||||
nodes: SerializedLexicalNode[]
|
nodes: SerializedLexicalNode[]
|
||||||
payloadConfig: SanitizedConfig
|
|
||||||
validation: {
|
validation: {
|
||||||
options: ValidateOptions<SerializedEditorState, unknown, RichTextField>
|
options: ValidateOptions<unknown, unknown, RichTextField>
|
||||||
value: SerializedEditorState
|
value: SerializedEditorState
|
||||||
}
|
}
|
||||||
}): Promise<string | true> {
|
}): Promise<string | true> {
|
||||||
@@ -30,7 +27,6 @@ export async function validateNodes({
|
|||||||
const validationResult = await validation({
|
const validationResult = await validation({
|
||||||
node,
|
node,
|
||||||
nodeValidations,
|
nodeValidations,
|
||||||
payloadConfig,
|
|
||||||
validation: validationFromProps,
|
validation: validationFromProps,
|
||||||
})
|
})
|
||||||
if (validationResult !== true) {
|
if (validationResult !== true) {
|
||||||
@@ -44,7 +40,6 @@ export async function validateNodes({
|
|||||||
const childrenValidationResult = await validateNodes({
|
const childrenValidationResult = await validateNodes({
|
||||||
nodeValidations,
|
nodeValidations,
|
||||||
nodes: node.children as SerializedLexicalNode[],
|
nodes: node.children as SerializedLexicalNode[],
|
||||||
payloadConfig,
|
|
||||||
validation: validationFromProps,
|
validation: validationFromProps,
|
||||||
})
|
})
|
||||||
if (childrenValidationResult !== true) {
|
if (childrenValidationResult !== true) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const LinkDrawer: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
return getFormState({
|
return await getFormState({
|
||||||
apiRoute: config.routes.api,
|
apiRoute: config.routes.api,
|
||||||
body: {
|
body: {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
AlignFeature,
|
||||||
|
BlockQuoteFeature,
|
||||||
|
BlocksFeature,
|
||||||
|
LinkFeature,
|
||||||
|
lexicalEditor,
|
||||||
|
} from '@payloadcms/richtext-lexical'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import type { Config, SanitizedConfig } from '../packages/payload/src/config/types'
|
import type { Config, SanitizedConfig } from '../packages/payload/src/config/types'
|
||||||
@@ -5,7 +12,6 @@ import type { Config, SanitizedConfig } from '../packages/payload/src/config/typ
|
|||||||
import { mongooseAdapter } from '../packages/db-mongodb/src'
|
import { mongooseAdapter } from '../packages/db-mongodb/src'
|
||||||
import { postgresAdapter } from '../packages/db-postgres/src'
|
import { postgresAdapter } from '../packages/db-postgres/src'
|
||||||
import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build'
|
import { buildConfig as buildPayloadConfig } from '../packages/payload/src/config/build'
|
||||||
import { AlignFeature, LinkFeature, lexicalEditor } from '../packages/richtext-lexical/src'
|
|
||||||
//import { slateEditor } from '../packages/richtext-slate/src'
|
//import { slateEditor } from '../packages/richtext-slate/src'
|
||||||
|
|
||||||
// process.env.PAYLOAD_DATABASE = 'postgres'
|
// process.env.PAYLOAD_DATABASE = 'postgres'
|
||||||
@@ -73,7 +79,28 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
|||||||
},
|
},
|
||||||
}),*/
|
}),*/
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
features: [LinkFeature({}), AlignFeature()],
|
features: [
|
||||||
|
LinkFeature({}),
|
||||||
|
AlignFeature(),
|
||||||
|
BlockQuoteFeature(),
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
slug: 'myBlock',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
max: 9999999999,
|
max: 9999999999,
|
||||||
|
|||||||
Reference in New Issue
Block a user