From 63cc9668dfc7bc8522be689645d31d7db81cf58d Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Sat, 16 Nov 2024 15:30:18 -0700 Subject: [PATCH] feat(richtext-lexical): allow replacing entire blocks with custom components (#9234) With this PR, you can now customize the way that `blocks` and `inlineBlocks` are rendered within Lexical's `BlocksFeature` by passing your own React components. This is super helpful when you need to create "previews" or more accurate UI for your Lexical blocks. For example, let's say you have a `gallery` block where your admins select a bunch of images. By default, Lexical would just render a collapsible with your block's fields in it. But now you can customize the `admin.components.Block` property on your `block` config by passing it a custom React component for us to render instead. So using that, with this `gallery` example, you could make a dynamic gallery React component that shows the images to your editors - and then render our built-in `BlockEditButton` to allow your editors to manage your gallery in a drawer. Here is an example where the BlockEditButton is added to the default Block Collapsible/Header: ![image](https://github.com/user-attachments/assets/db8c13f1-2650-4b33-bc11-2582bb937f3d) --------- Co-authored-by: James --- docs/fields/blocks.mdx | 45 +++ packages/payload/src/admin/forms/Form.ts | 5 + .../bin/generateImportMap/iterateFields.ts | 19 + packages/payload/src/fields/config/types.ts | 29 +- .../src/exports/client/index.ts | 8 + .../blocks/client/component/BlockContent.tsx | 311 ++++++---------- .../client/component/FormSavePlugin.tsx | 114 +++++- .../component/components/BlockCollapsible.tsx | 23 ++ .../component/components/BlockEditButton.tsx | 10 + .../components/BlockRemoveButton.tsx | 10 + .../blocks/client/component/index.scss | 14 +- .../blocks/client/component/index.tsx | 333 +++++++++++++++--- .../components/InlineBlockContainer.tsx | 10 + .../components/InlineBlockEditButton.tsx | 10 + .../components/InlineBlockLabel.tsx | 10 + .../components/InlineBlockRemoveButton.tsx | 10 + .../blocks/client/componentInline/index.tsx | 332 ++++++++++++++--- .../src/features/blocks/client/index.tsx | 17 +- .../features/blocks/client/plugin/commands.ts | 5 - .../features/blocks/client/plugin/index.tsx | 95 +---- .../src/features/blocks/server/index.ts | 33 +- .../toolbars/fixed/client/Toolbar/index.scss | 1 + .../fieldsDrawer/useLexicalDrawer.tsx | 18 +- packages/ui/src/forms/Form/index.tsx | 9 +- packages/ui/src/forms/Submit/index.tsx | 18 +- .../fieldSchemasToFormState/renderField.tsx | 29 ++ packages/ui/src/utilities/buildTableState.ts | 2 +- .../blockComponents/BlockComponent.tsx | 20 ++ .../blockComponents/LabelComponent.tsx | 10 + test/fields/collections/Lexical/index.ts | 126 ++++++- .../inlineBlockComponents/BlockComponent.tsx | 18 + .../inlineBlockComponents/LabelComponent.tsx | 10 + 32 files changed, 1230 insertions(+), 474 deletions(-) create mode 100644 packages/richtext-lexical/src/features/blocks/client/component/components/BlockCollapsible.tsx create mode 100644 packages/richtext-lexical/src/features/blocks/client/component/components/BlockEditButton.tsx create mode 100644 packages/richtext-lexical/src/features/blocks/client/component/components/BlockRemoveButton.tsx create mode 100644 packages/richtext-lexical/src/features/blocks/client/componentInline/components/InlineBlockContainer.tsx create mode 100644 packages/richtext-lexical/src/features/blocks/client/componentInline/components/InlineBlockEditButton.tsx create mode 100644 packages/richtext-lexical/src/features/blocks/client/componentInline/components/InlineBlockLabel.tsx create mode 100644 packages/richtext-lexical/src/features/blocks/client/componentInline/components/InlineBlockRemoveButton.tsx create mode 100644 test/fields/collections/Lexical/blockComponents/BlockComponent.tsx create mode 100644 test/fields/collections/Lexical/blockComponents/LabelComponent.tsx create mode 100644 test/fields/collections/Lexical/inlineBlockComponents/BlockComponent.tsx create mode 100644 test/fields/collections/Lexical/inlineBlockComponents/LabelComponent.tsx diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index e4c5c514b6..43b361de65 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -84,6 +84,51 @@ The Blocks Field inherits all of the default options from the base [Field Admin | **`initCollapsed`** | Set the initial collapsed state | | **`isSortable`** | Disable order sorting by setting this value to `false` | +#### Customizing the way your block is rendered in Lexical + +If you're using this block within the [Lexical editor](/docs/lexical/overview), you can also customize how the block is rendered in the Lexical editor itself by specifying custom components. + +- `admin.components.Label` - pass a custom React component here to customize the way that the label is rendered for this block +- `admin.components.Block` - pass a component here to completely override the way the block is rendered in Lexical with your own component + +This is super handy if you'd like to present your editors with a very deliberate and nicely designed block "preview" right in your rich text. + +For example, if you have a `gallery` block, you might want to actually render the gallery of images directly in your Lexical block. With the `admin.components.Block` property, you can do exactly that! + + + Tip:
+ If you customize the way your block is rendered in Lexical, you can import utility components to easily edit / remove your block - so that you don't have to build all of this yourself. +
+ +To import these utility components for one of your custom blocks, you can import the following: + +```ts +import { + // Edit block buttons (choose the one that corresponds to your usage) + // When clicked, this will open a drawer with your block's fields + // so your editors can edit them + InlineBlockEditButton, + BlockEditButton, + + // Buttons that will remove this block from Lexical + // (choose the one that corresponds to your usage) + InlineBlockRemoveButton, + BlockRemoveButton, + + // The label that should be rendered for an inline block + InlineBlockLabel, + + // The default "container" that is rendered for an inline block + // if you want to re-use it + InlineBlockContainer, + + // The default "collapsible" UI that is rendered for a regular block + // if you want to re-use it + BlockCollapsible, + +} from '@payloadcms/richtext-lexical/client' +``` + ## Block Configs Blocks are defined as separate configs of their own. diff --git a/packages/payload/src/admin/forms/Form.ts b/packages/payload/src/admin/forms/Form.ts index bd96fadb53..6cfe651840 100644 --- a/packages/payload/src/admin/forms/Form.ts +++ b/packages/payload/src/admin/forms/Form.ts @@ -22,6 +22,11 @@ export type FilterOptionsResult = { export type FieldState = { customComponents?: { + /** + * This is used by UI fields, as they can have arbitrary components defined if used + * as a vessel to bring in custom components. + */ + [key: string]: React.ReactNode | React.ReactNode[] | undefined AfterInput?: React.ReactNode BeforeInput?: React.ReactNode Description?: React.ReactNode diff --git a/packages/payload/src/bin/generateImportMap/iterateFields.ts b/packages/payload/src/bin/generateImportMap/iterateFields.ts index 0d461cd09c..1dcc387fbb 100644 --- a/packages/payload/src/bin/generateImportMap/iterateFields.ts +++ b/packages/payload/src/bin/generateImportMap/iterateFields.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ import type { PayloadComponent, SanitizedConfig } from '../../config/types.js' import type { Block, Field, Tab } from '../../fields/config/types.js' import type { AddToImportMap, Imports, InternalImportMap } from './index.js' @@ -9,6 +10,12 @@ function hasKey( return obj != null && Object.prototype.hasOwnProperty.call(obj, key) } +const defaultUIFieldComponentKeys: Array<'Cell' | 'Description' | 'Field' | 'Filter'> = [ + 'Cell', + 'Description', + 'Field', + 'Filter', +] export function genImportMapIterateFields({ addToImportMap, baseDir, @@ -67,10 +74,22 @@ export function genImportMapIterateFields({ imports, }) } + } else if (field.type === 'ui') { + if (field?.admin?.components) { + // Render any extra, untyped components + for (const key in field.admin.components) { + if (key in defaultUIFieldComponentKeys) { + continue + } + addToImportMap(field.admin.components[key]) + } + } } hasKey(field?.admin?.components, 'Label') && addToImportMap(field.admin.components.Label) + hasKey(field?.admin?.components, 'Block') && addToImportMap(field.admin.components.Block) + hasKey(field?.admin?.components, 'Cell') && addToImportMap(field?.admin?.components?.Cell) hasKey(field?.admin?.components, 'Description') && diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index e77470668b..0a455225ca 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -748,8 +748,14 @@ export type TabAsFieldClient = ClientTab & Pick export type UIField = { admin: { components?: { + /** + * Allow any custom components to be added to the UI field. This allows + * the UI field to be used as a vessel for getting components rendered. + */ + [key: string]: PayloadComponent | undefined Cell?: CustomComponent - Field: CustomComponent + // Can be optional, in case the UI field is just used as a vessel for custom components + Field?: CustomComponent /** * The Filter component has to be a client component */ @@ -1188,16 +1194,11 @@ export type Block = { _sanitized?: boolean admin?: { components?: { - Label?: PayloadComponent< - never, - { - blockKind: 'block' | 'lexicalBlock' | 'lexicalInlineBlock' | string - /** - * May contain the formData - */ - formData: Record - } - > + /** + * This will replace the entire block component, including the block header / collapsible. + */ + Block?: PayloadComponent + Label?: PayloadComponent } /** Extension point to add your custom data. Available in server and client. */ custom?: Record @@ -1227,11 +1228,7 @@ export type Block = { } export type ClientBlock = { - admin?: { - components?: { - Label?: React.ReactNode - } - } & Pick + admin?: Pick fields: ClientField[] labels?: LabelsClient } & Pick diff --git a/packages/richtext-lexical/src/exports/client/index.ts b/packages/richtext-lexical/src/exports/client/index.ts index 0d217b9ce7..49c6166394 100644 --- a/packages/richtext-lexical/src/exports/client/index.ts +++ b/packages/richtext-lexical/src/exports/client/index.ts @@ -128,3 +128,11 @@ export { } from '../../features/blocks/client/nodes/InlineBlocksNode.js' export { FieldsDrawer } from '../../utilities/fieldsDrawer/Drawer.js' + +export { InlineBlockEditButton } from '../../features/blocks/client/componentInline/components/InlineBlockEditButton.js' +export { InlineBlockRemoveButton } from '../../features/blocks/client/componentInline/components/InlineBlockRemoveButton.js' +export { InlineBlockLabel } from '../../features/blocks/client/componentInline/components/InlineBlockLabel.js' +export { InlineBlockContainer } from '../../features/blocks/client/componentInline/components/InlineBlockContainer.js' +export { BlockCollapsible } from '../../features/blocks/client/component/components/BlockCollapsible.js' +export { BlockEditButton } from '../../features/blocks/client/component/components/BlockEditButton.js' +export { BlockRemoveButton } from '../../features/blocks/client/component/components/BlockRemoveButton.js' diff --git a/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx b/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx index 5599049348..83de21dda5 100644 --- a/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/component/BlockContent.tsx @@ -1,239 +1,128 @@ 'use client' -import type { ClientBlock, ClientField, CollapsedPreferences, FormState } from 'payload' +import type { ClientField, FormState } from 'payload' -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' -import { getTranslation } from '@payloadcms/translations' -import { - Button, - Collapsible, - ErrorPill, - Pill, - RenderFields, - SectionTitle, - useDocumentInfo, - useFormSubmitted, - useTranslation, -} from '@payloadcms/ui' -import { dequal } from 'dequal/lite' -import { $getNodeByKey } from 'lexical' -import React, { useCallback, useEffect } from 'react' +import { RenderFields } from '@payloadcms/ui' +import React, { createContext, useMemo } from 'react' -import type { LexicalRichTextFieldProps } from '../../../../types.js' import type { BlockFields } from '../../server/nodes/BlocksNode.js' -import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' -import { $isBlockNode } from '../nodes/BlocksNode.js' -import { FormSavePlugin } from './FormSavePlugin.js' +import { useFormSave } from './FormSavePlugin.js' type Props = { baseClass: string - clientBlock: ClientBlock - field: LexicalRichTextFieldProps['field'] + BlockDrawer: React.FC + Collapsible: React.FC<{ + children?: React.ReactNode + editButton?: boolean + errorCount?: number + fieldHasErrors?: boolean + /** + * Override the default label with a custom label + */ + Label?: React.ReactNode + removeButton?: boolean + }> + CustomBlock: React.ReactNode + EditButton: React.FC formData: BlockFields formSchema: ClientField[] - Label?: React.ReactNode + initialState: false | FormState | undefined nodeKey: string - path: string - schemaPath: string + + RemoveButton: React.FC } -// 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 -// does not have, even if it's undefined. -// 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. -function removeUndefinedAndNullAndEmptyArraysRecursively(obj: object) { - for (const key in obj) { - const value = obj[key] - if (Array.isArray(value) && !value?.length) { - delete obj[key] - } else if (value && typeof value === 'object') { - removeUndefinedAndNullAndEmptyArraysRecursively(value) - } else if (value === undefined || value === null) { - delete obj[key] - } - } +type BlockComponentContextType = { + BlockCollapsible?: React.FC<{ + children?: React.ReactNode + editButton?: boolean + /** + * Override the default label with a custom label + */ + Label?: React.ReactNode + removeButton?: boolean + }> + EditButton?: React.FC + initialState: false | FormState | undefined + + nodeKey?: string + RemoveButton?: React.FC } +const BlockComponentContext = createContext({ + initialState: false, +}) + +export const useBlockComponentContext = () => React.useContext(BlockComponentContext) + /** * The actual content of the Block. This should be INSIDE a Form component, * scoped to the block. All format operations in here are thus scoped to the block's form, and * not the whole document. */ export const BlockContent: React.FC = (props) => { - const { baseClass, clientBlock, field, formSchema, Label, nodeKey } = props - let { formData } = props + const { + BlockDrawer, + Collapsible, + CustomBlock, + EditButton, + formData, + formSchema, + initialState, + nodeKey, + RemoveButton, + } = props - const { i18n } = useTranslation() - const [editor] = useLexicalComposerContext() - // 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 { errorCount, fieldHasErrors } = useFormSave({ disabled: !initialState, formData, nodeKey }) - const [isCollapsed, setIsCollapsed] = React.useState() + const CollapsibleWithErrorProps = useMemo( + () => + (props: { + children?: React.ReactNode + editButton?: boolean - useEffect(() => { - void getDocPreferences().then((currentDocPreferences) => { - const currentFieldPreferences = currentDocPreferences?.fields[field.name] - const collapsedArray = currentFieldPreferences?.collapsed - setIsCollapsed(collapsedArray ? collapsedArray.includes(formData.id) : false) - }) - }, [field.name, formData.id, getDocPreferences]) - - const hasSubmitted = useFormSubmitted() - - const [errorCount, setErrorCount] = React.useState(0) - - const fieldHasErrors = hasSubmitted && errorCount > 0 - - const classNames = [ - `${baseClass}__row`, - fieldHasErrors ? `${baseClass}__row--has-errors` : `${baseClass}__row--no-errors`, - ] - .filter(Boolean) - .join(' ') - - const onFormChange = useCallback( - ({ - fullFieldsWithValues, - newFormData, - }: { - fullFieldsWithValues: FormState - newFormData: BlockFields - }) => { - newFormData.id = formData.id - newFormData.blockType = formData.blockType - - removeUndefinedAndNullAndEmptyArraysRecursively(newFormData) - removeUndefinedAndNullAndEmptyArraysRecursively(formData) - - // 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 - if (!dequal(formData, newFormData)) { - // Running this in the next tick in the meantime fixes this issue: https://github.com/payloadcms/payload/issues/4108 - // I don't know why. When this is called immediately, it might focus out of a nested lexical editor field if an update is made there. - // My hypothesis is that the nested editor might not have fully finished its update cycle yet. By updating in the next tick, we - // ensure that the nested editor has finished its update cycle before we update the block node. - setTimeout(() => { - editor.update(() => { - const node = $getNodeByKey(nodeKey) - if (node && $isBlockNode(node)) { - formData = newFormData - node.setFields(newFormData) - } - }) - }, 0) - } - - // update error count - if (hasSubmitted) { - let rowErrorCount = 0 - for (const formField of Object.values(fullFieldsWithValues)) { - if (formField?.valid === false) { - rowErrorCount++ - } - } - setErrorCount(rowErrorCount) - } - }, - [editor, nodeKey, hasSubmitted, formData], + /** + * Override the default label with a custom label + */ + Label?: React.ReactNode + removeButton?: boolean + }) => ( + + {props.children} + + ), + [Collapsible, fieldHasErrors, errorCount], ) - const onCollapsedChange = useCallback( - (changedCollapsed: boolean) => { - void getDocPreferences().then((currentDocPreferences) => { - const currentFieldPreferences = currentDocPreferences?.fields[field.name] - - const collapsedArray = currentFieldPreferences?.collapsed - - const newCollapsed: CollapsedPreferences = - collapsedArray && collapsedArray?.length ? collapsedArray : [] - - if (changedCollapsed) { - if (!newCollapsed.includes(formData.id)) { - newCollapsed.push(formData.id) - } - } else { - if (newCollapsed.includes(formData.id)) { - newCollapsed.splice(newCollapsed.indexOf(formData.id), 1) - } - } - - setDocFieldPreferences(field.name, { - collapsed: newCollapsed, - hello: 'hi', - }) - }) - }, - [getDocPreferences, field.name, setDocFieldPreferences, formData.id], - ) - - const removeBlock = useCallback(() => { - editor.update(() => { - $getNodeByKey(nodeKey)?.remove() - }) - }, [editor, nodeKey]) - - if (typeof isCollapsed !== 'boolean') { - return null - } - - return ( - - -
- - {typeof clientBlock?.labels?.singular === 'string' - ? getTranslation(clientBlock?.labels.singular, i18n) - : clientBlock.slug} - - - {fieldHasErrors && } -
- {editor.isEditable() && ( - diff --git a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx index 16935173eb..82796cd9fa 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx @@ -16,6 +16,13 @@ import type { RenderFieldMethod } from './types.js' import { RenderServerComponent } from '../../elements/RenderServerComponent/index.js' import { FieldDescription } from '../../fields/FieldDescription/index.js' +const defaultUIFieldComponentKeys: Array<'Cell' | 'Description' | 'Field' | 'Filter'> = [ + 'Cell', + 'Description', + 'Field', + 'Filter', +] + export const renderField: RenderFieldMethod = ({ data, fieldConfig, @@ -157,6 +164,28 @@ export const renderField: RenderFieldMethod = ({ break } + case 'ui': { + if (fieldConfig?.admin?.components) { + // Render any extra, untyped components + for (const key in fieldConfig.admin.components) { + if (key in defaultUIFieldComponentKeys) { + continue + } + const Component = fieldConfig.admin.components[key] + fieldState.customComponents[key] = ( + + ) + } + } + break + } + default: { break } diff --git a/packages/ui/src/utilities/buildTableState.ts b/packages/ui/src/utilities/buildTableState.ts index 8c2d10dbdb..050100f3a2 100644 --- a/packages/ui/src/utilities/buildTableState.ts +++ b/packages/ui/src/utilities/buildTableState.ts @@ -9,7 +9,7 @@ import type { SanitizedConfig, } from 'payload' -import { dequal } from 'dequal' +import { dequal } from 'dequal' // TODO: Can we change this to dequal/lite ? If not, please add comment explaining why import { createClientConfig, formatErrors } from 'payload' import type { Column } from '../elements/Table/index.js' diff --git a/test/fields/collections/Lexical/blockComponents/BlockComponent.tsx b/test/fields/collections/Lexical/blockComponents/BlockComponent.tsx new file mode 100644 index 0000000000..92c60b4aab --- /dev/null +++ b/test/fields/collections/Lexical/blockComponents/BlockComponent.tsx @@ -0,0 +1,20 @@ +'use client' +import { + BlockCollapsible, + BlockEditButton, + BlockRemoveButton, +} from '@payloadcms/richtext-lexical/client' +import { useFormFields } from '@payloadcms/ui' +import React from 'react' + +export const BlockComponent: React.FC = () => { + const key = useFormFields(([fields]) => fields.key) + + return ( + + MY BLOCK COMPONENT. Value: {(key?.value as string) ?? ''} + Edit: + + + ) +} diff --git a/test/fields/collections/Lexical/blockComponents/LabelComponent.tsx b/test/fields/collections/Lexical/blockComponents/LabelComponent.tsx new file mode 100644 index 0000000000..9364e90ce2 --- /dev/null +++ b/test/fields/collections/Lexical/blockComponents/LabelComponent.tsx @@ -0,0 +1,10 @@ +'use client' + +import { useFormFields } from '@payloadcms/ui' +import React from 'react' + +export const LabelComponent: React.FC = () => { + const key = useFormFields(([fields]) => fields.key) + + return
{(key?.value as string) ?? ''}yaya
+} diff --git a/test/fields/collections/Lexical/index.ts b/test/fields/collections/Lexical/index.ts index 21a191f8de..6c95443b6c 100644 --- a/test/fields/collections/Lexical/index.ts +++ b/test/fields/collections/Lexical/index.ts @@ -82,13 +82,137 @@ const editorConfig: ServerEditorConfig = { ConditionalLayoutBlock, TabBlock, CodeBlock, + { + slug: 'myBlock', + admin: { + components: {}, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, + { + slug: 'myBlockWithLabel', + admin: { + components: { + Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent', + }, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, + { + slug: 'myBlockWithBlock', + admin: { + components: { + Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent', + }, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, + { + slug: 'myBlockWithBlockAndLabel', + admin: { + components: { + Block: '/collections/Lexical/blockComponents/BlockComponent.js#BlockComponent', + Label: '/collections/Lexical/blockComponents/LabelComponent.js#LabelComponent', + }, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, ], inlineBlocks: [ { slug: 'myInlineBlock', + admin: { + components: {}, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, + { + slug: 'myInlineBlockWithLabel', admin: { components: { - Label: '/collections/Lexical/LabelComponent.js#LabelComponent', + Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent', + }, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, + { + slug: 'myInlineBlockWithBlock', + admin: { + components: { + Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent', + }, + }, + fields: [ + { + name: 'key', + label: () => { + return 'Key' + }, + type: 'select', + options: ['value1', 'value2', 'value3'], + }, + ], + }, + { + slug: 'myInlineBlockWithBlockAndLabel', + admin: { + components: { + Block: '/collections/Lexical/inlineBlockComponents/BlockComponent.js#BlockComponent', + Label: '/collections/Lexical/inlineBlockComponents/LabelComponent.js#LabelComponent', }, }, fields: [ diff --git a/test/fields/collections/Lexical/inlineBlockComponents/BlockComponent.tsx b/test/fields/collections/Lexical/inlineBlockComponents/BlockComponent.tsx new file mode 100644 index 0000000000..786cb37714 --- /dev/null +++ b/test/fields/collections/Lexical/inlineBlockComponents/BlockComponent.tsx @@ -0,0 +1,18 @@ +import { + InlineBlockContainer, + InlineBlockEditButton, + InlineBlockLabel, + InlineBlockRemoveButton, +} from '@payloadcms/richtext-lexical/client' +import React from 'react' + +export const BlockComponent: React.FC = () => { + return ( + +

Test

+ + + +
+ ) +} diff --git a/test/fields/collections/Lexical/inlineBlockComponents/LabelComponent.tsx b/test/fields/collections/Lexical/inlineBlockComponents/LabelComponent.tsx new file mode 100644 index 0000000000..9364e90ce2 --- /dev/null +++ b/test/fields/collections/Lexical/inlineBlockComponents/LabelComponent.tsx @@ -0,0 +1,10 @@ +'use client' + +import { useFormFields } from '@payloadcms/ui' +import React from 'react' + +export const LabelComponent: React.FC = () => { + const key = useFormFields(([fields]) => fields.key) + + return
{(key?.value as string) ?? ''}yaya
+}