feat!: on demand rsc (#8364)
Currently, Payload renders all custom components on initial compile of the admin panel. This is problematic for two key reasons: 1. Custom components do not receive contextual data, i.e. fields do not receive their field data, edit views do not receive their document data, etc. 2. Components are unnecessarily rendered before they are used This was initially required to support React Server Components within the Payload Admin Panel for two key reasons: 1. Fields can be dynamically rendered within arrays, blocks, etc. 2. Documents can be recursively rendered within a "drawer" UI, i.e. relationship fields 3. Payload supports server/client component composition In order to achieve this, components need to be rendered on the server and passed as "slots" to the client. Currently, the pattern for this is to render custom server components in the "client config". Then when a view or field is needed to be rendered, we first check the client config for a "pre-rendered" component, otherwise render our client-side fallback component. But for the reasons listed above, this pattern doesn't exactly make custom server components very useful within the Payload Admin Panel, which is where this PR comes in. Now, instead of pre-rendering all components on initial compile, we're able to render custom components _on demand_, only as they are needed. To achieve this, we've established [this pattern](https://github.com/payloadcms/payload/pull/8481) of React Server Functions in the Payload Admin Panel. With Server Functions, we can iterate the Payload Config and return JSX through React's `text/x-component` content-type. This means we're able to pass contextual props to custom components, such as data for fields and views. ## Breaking Changes 1. Add the following to your root layout file, typically located at `(app)/(payload)/layout.tsx`: ```diff /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ + import type { ServerFunctionClient } from 'payload' import config from '@payload-config' import { RootLayout } from '@payloadcms/next/layouts' import { handleServerFunctions } from '@payloadcms/next/utilities' import React from 'react' import { importMap } from './admin/importMap.js' import './custom.scss' type Args = { children: React.ReactNode } + const serverFunctions: ServerFunctionClient = async function (args) { + 'use server' + return handleServerFunctions({ + ...args, + config, + importMap, + }) + } const Layout = ({ children }: Args) => ( <RootLayout config={config} importMap={importMap} + serverFunctions={serverFunctions} > {children} </RootLayout> ) export default Layout ``` 2. If you were previously posting to the `/api/form-state` endpoint, it no longer exists. Instead, you'll need to invoke the `form-state` Server Function, which can be done through the _new_ `getFormState` utility: ```diff - import { getFormState } from '@payloadcms/ui' - const { state } = await getFormState({ - apiRoute: '', - body: { - // ... - }, - serverURL: '' - }) + const { getFormState } = useServerFunctions() + + const { state } = await getFormState({ + // ... + }) ``` ## Breaking Changes ```diff - useFieldProps() - useCellProps() ``` More details coming soon. --------- Co-authored-by: Alessio Gravili <alessio@gravili.de> Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com> Co-authored-by: James <james@trbl.design>
This commit is contained in:
@@ -17,11 +17,15 @@
|
||||
"types": "./src/index.tsx",
|
||||
"default": "./src/index.tsx"
|
||||
},
|
||||
"./generateComponentMap": "./src/generateComponentMap.tsx",
|
||||
"./client": {
|
||||
"import": "./src/exports/client/index.ts",
|
||||
"types": "./src/exports/client/index.ts",
|
||||
"default": "./src/exports/client/index.ts"
|
||||
},
|
||||
"./rsc": {
|
||||
"import": "./src/exports/server/rsc.ts",
|
||||
"types": "./src/exports/server/rsc.ts",
|
||||
"default": "./src/exports/server/rsc.ts"
|
||||
}
|
||||
},
|
||||
"main": "./src/index.tsx",
|
||||
@@ -70,10 +74,10 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./generateComponentMap": {
|
||||
"import": "./dist/generateComponentMap.js",
|
||||
"types": "./dist/generateComponentMap.d.ts",
|
||||
"default": "./dist/generateComponentMap.js"
|
||||
"./rsc": {
|
||||
"import": "./dist/exports/server/rsc.js",
|
||||
"types": "./dist/exports/server/rsc.d.ts",
|
||||
"default": "./dist/exports/server/rsc.js"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/exports/client/index.js",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
'use client'
|
||||
import type { DefaultCellComponentProps } from 'payload'
|
||||
|
||||
import { useConfig, useTableCell } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const RichTextCell: React.FC<DefaultCellComponentProps<any[]>> = () => {
|
||||
const { cellData, cellProps, columnIndex, customCellContext, rowData } = useTableCell()
|
||||
const flattenedText = cellData?.map((i) => i?.children?.map((c) => c.text)).join(' ')
|
||||
|
||||
const {
|
||||
config: {
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
const { link } = cellProps || {}
|
||||
let WrapElement: React.ComponentType<any> | string = 'span'
|
||||
|
||||
const wrapElementProps: {
|
||||
className?: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
type?: 'button'
|
||||
} = {}
|
||||
|
||||
const isLink = link !== undefined ? link : columnIndex === 0
|
||||
|
||||
if (isLink) {
|
||||
WrapElement = Link
|
||||
wrapElementProps.href = customCellContext?.collectionSlug
|
||||
? formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${customCellContext?.collectionSlug}/${rowData.id}`,
|
||||
})
|
||||
: ''
|
||||
}
|
||||
|
||||
if (isLink) {
|
||||
return <WrapElement {...wrapElementProps}>{flattenedText}</WrapElement>
|
||||
}
|
||||
// Limiting the number of characters shown is done in a CSS rule
|
||||
return <span>{flattenedText}</span>
|
||||
}
|
||||
87
packages/richtext-slate/src/cell/rscEntry.tsx
Normal file
87
packages/richtext-slate/src/cell/rscEntry.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { DefaultCellComponentProps, Payload } from 'payload'
|
||||
|
||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const RscEntrySlateCell: React.FC<
|
||||
{
|
||||
i18n: I18nClient
|
||||
payload: Payload
|
||||
} & DefaultCellComponentProps
|
||||
> = (props) => {
|
||||
const {
|
||||
cellData,
|
||||
className: classNameFromProps,
|
||||
collectionConfig,
|
||||
field: { admin },
|
||||
field,
|
||||
i18n,
|
||||
link,
|
||||
onClick: onClickFromProps,
|
||||
payload,
|
||||
rowData,
|
||||
} = props
|
||||
|
||||
const classNameFromConfigContext = admin && 'className' in admin ? admin.className : undefined
|
||||
|
||||
const className =
|
||||
classNameFromProps ||
|
||||
(field.admin && 'className' in field.admin ? field.admin.className : null) ||
|
||||
classNameFromConfigContext
|
||||
const adminRoute = payload.config.routes.admin
|
||||
|
||||
const onClick = onClickFromProps
|
||||
|
||||
let WrapElement: React.ComponentType<any> | string = 'span'
|
||||
|
||||
const wrapElementProps: {
|
||||
className?: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
prefetch?: false
|
||||
type?: 'button'
|
||||
} = {
|
||||
className,
|
||||
}
|
||||
|
||||
if (link) {
|
||||
wrapElementProps.prefetch = false
|
||||
WrapElement = Link
|
||||
wrapElementProps.href = collectionConfig?.slug
|
||||
? formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionConfig?.slug}/${rowData.id}`,
|
||||
})
|
||||
: ''
|
||||
}
|
||||
|
||||
if (typeof onClick === 'function') {
|
||||
WrapElement = 'button'
|
||||
wrapElementProps.type = 'button'
|
||||
wrapElementProps.onClick = () => {
|
||||
onClick({
|
||||
cellData,
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
rowData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let textContent = ''
|
||||
|
||||
if (cellData) {
|
||||
textContent = cellData?.map((i) => i?.children?.map((c) => c.text)).join(' ')
|
||||
}
|
||||
|
||||
if (!cellData || !textContent?.length) {
|
||||
textContent = i18n.t('general:noLabel', {
|
||||
label: getTranslation(('label' in field ? field.label : null) || 'data', i18n),
|
||||
})
|
||||
}
|
||||
|
||||
return <WrapElement {...wrapElementProps}>{textContent}</WrapElement>
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
export { RichTextCell } from '../../cell/index.js'
|
||||
export { BlockquoteElementButton } from '../../field/elements/blockquote/Button.js'
|
||||
export { BlockquoteElement } from '../../field/elements/blockquote/Element.js'
|
||||
export { ElementButton } from '../../field/elements/Button.js'
|
||||
@@ -55,11 +54,11 @@ export { CodeLeafButton } from '../../field/leaves/code/LeafButton.js'
|
||||
|
||||
export { ItalicLeaf } from '../../field/leaves/italic/Italic/index.js'
|
||||
export { ItalicLeafButton } from '../../field/leaves/italic/LeafButton.js'
|
||||
|
||||
export { StrikethroughLeafButton } from '../../field/leaves/strikethrough/LeafButton.js'
|
||||
|
||||
export { StrikethroughLeaf } from '../../field/leaves/strikethrough/Strikethrough/index.js'
|
||||
export { UnderlineLeafButton } from '../../field/leaves/underline/LeafButton.js'
|
||||
|
||||
export { UnderlineLeafButton } from '../../field/leaves/underline/LeafButton.js'
|
||||
export { UnderlineLeaf } from '../../field/leaves/underline/Underline/index.js'
|
||||
|
||||
export { useElement } from '../../field/providers/ElementProvider.js'
|
||||
|
||||
2
packages/richtext-slate/src/exports/server/rsc.ts
Normal file
2
packages/richtext-slate/src/exports/server/rsc.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { RscEntrySlateCell } from '../../cell/rscEntry.js'
|
||||
export { RscEntrySlateField } from '../../field/rscEntry.js'
|
||||
@@ -6,17 +6,7 @@ import type { HistoryEditor } from 'slate-history'
|
||||
import type { ReactEditor } from 'slate-react'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
RenderComponent,
|
||||
useEditDepth,
|
||||
useField,
|
||||
useFieldProps,
|
||||
useTranslation,
|
||||
withCondition,
|
||||
} from '@payloadcms/ui'
|
||||
import { FieldLabel, useEditDepth, useField, useTranslation, withCondition } from '@payloadcms/ui'
|
||||
import { isHotkey } from 'is-hotkey'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { createEditor, Node, Element as SlateElement, Text, Transforms } from 'slate'
|
||||
@@ -51,30 +41,24 @@ declare module 'slate' {
|
||||
|
||||
const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
const {
|
||||
descriptionProps,
|
||||
elements,
|
||||
errorProps,
|
||||
field,
|
||||
field: {
|
||||
name,
|
||||
_path: pathFromProps,
|
||||
admin: {
|
||||
className,
|
||||
components: { Description, Error, Label },
|
||||
placeholder,
|
||||
readOnly: readOnlyFromAdmin,
|
||||
style,
|
||||
width,
|
||||
} = {},
|
||||
admin: { className, placeholder, readOnly: readOnlyFromAdmin, style, width } = {},
|
||||
label,
|
||||
required,
|
||||
},
|
||||
labelProps,
|
||||
leaves,
|
||||
path: pathFromProps,
|
||||
plugins,
|
||||
readOnly: readOnlyFromTopLevelProps,
|
||||
schemaPath: schemaPathFromProps,
|
||||
validate = richTextValidate,
|
||||
} = props
|
||||
|
||||
const path = pathFromProps ?? name
|
||||
const schemaPath = schemaPathFromProps ?? name
|
||||
|
||||
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -99,16 +83,19 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
[validate, required, i18n],
|
||||
)
|
||||
|
||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||
const {
|
||||
customComponents: { Description, Error, Label } = {},
|
||||
formInitializing,
|
||||
initialValue,
|
||||
setValue,
|
||||
showError,
|
||||
value,
|
||||
} = useField({
|
||||
path,
|
||||
validate: memoizedValidate,
|
||||
})
|
||||
|
||||
const { formInitializing, initialValue, path, schemaPath, setValue, showError, value } = useField(
|
||||
{
|
||||
path: pathFromContext ?? pathFromProps ?? name,
|
||||
validate: memoizedValidate,
|
||||
},
|
||||
)
|
||||
|
||||
const disabled = readOnlyFromProps || readOnlyFromContext || formInitializing
|
||||
const disabled = readOnlyFromProps || formInitializing
|
||||
|
||||
const editor = useMemo(() => {
|
||||
let CreatedEditor = withEnterBreakOut(withHistory(withReact(createEditor())))
|
||||
@@ -179,7 +166,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
path={path}
|
||||
schemaPath={schemaPath}
|
||||
>
|
||||
<RenderComponent mappedComponent={Element} />
|
||||
{Element}
|
||||
</ElementProvider>
|
||||
)
|
||||
|
||||
@@ -212,7 +199,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
result={result}
|
||||
schemaPath={schemaPath}
|
||||
>
|
||||
<RenderComponent mappedComponent={Leaf} />
|
||||
{Leaf}
|
||||
</LeafProvider>
|
||||
)
|
||||
}
|
||||
@@ -320,9 +307,9 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
width,
|
||||
}}
|
||||
>
|
||||
<FieldLabel Label={Label} {...(labelProps || {})} field={field} />
|
||||
{Label || <FieldLabel label={label} required={required} />}
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<FieldError CustomError={Error} field={field} path={path} {...(errorProps || {})} />
|
||||
{Error}
|
||||
<Slate
|
||||
editor={editor}
|
||||
key={JSON.stringify({ initialValue, path })} // makes sure slate is completely re-rendered when initialValue changes, bypassing the slate-internal value memoization. That way, external changes to the form will update the editor
|
||||
@@ -350,7 +337,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
path={path}
|
||||
schemaPath={schemaPath}
|
||||
>
|
||||
<RenderComponent mappedComponent={Button} />
|
||||
{Button}
|
||||
</ElementButtonProvider>
|
||||
)
|
||||
}
|
||||
@@ -368,7 +355,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
path={path}
|
||||
schemaPath={schemaPath}
|
||||
>
|
||||
<RenderComponent mappedComponent={Button} />
|
||||
{Button}
|
||||
</LeafButtonProvider>
|
||||
)
|
||||
}
|
||||
@@ -453,7 +440,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Slate>
|
||||
<FieldDescription Description={Description} field={field} {...(descriptionProps || {})} />
|
||||
{Description}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MappedComponent } from 'payload'
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import type { EnabledFeatures } from './types.js'
|
||||
|
||||
export const createFeatureMap = (
|
||||
richTextComponentMap: Map<string, MappedComponent>,
|
||||
richTextComponentMap: Map<string, ClientField[] | React.ReactNode>,
|
||||
): EnabledFeatures => {
|
||||
const features: EnabledFeatures = {
|
||||
elements: {},
|
||||
@@ -12,6 +12,9 @@ export const createFeatureMap = (
|
||||
}
|
||||
|
||||
for (const [key, value] of richTextComponentMap) {
|
||||
if (Array.isArray(value)) {
|
||||
continue // We only wanna process react nodes here
|
||||
}
|
||||
if (key.startsWith('leaf.button') || key.startsWith('leaf.component.')) {
|
||||
const leafName = key.replace('leaf.button.', '').replace('leaf.component.', '')
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
import type { FormState } from 'payload'
|
||||
|
||||
import { useConfig, useDrawerSlug, useFieldProps, useModal, useTranslation } from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import {
|
||||
useDocumentInfo,
|
||||
useDrawerSlug,
|
||||
useModal,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { reduceFieldsToValues } from 'payload/shared'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Editor, Range, Transforms } from 'slate'
|
||||
@@ -55,23 +60,23 @@ const insertLink = (editor, fields) => {
|
||||
ReactEditor.focus(editor)
|
||||
}
|
||||
|
||||
export const LinkButton: React.FC = () => {
|
||||
export const LinkButton: React.FC<{
|
||||
schemaPath: string
|
||||
}> = ({ schemaPath }) => {
|
||||
const { fieldProps } = useElementButton()
|
||||
const [initialState, setInitialState] = useState<FormState>({})
|
||||
|
||||
const { t } = useTranslation()
|
||||
const editor = useSlate()
|
||||
const { config } = useConfig()
|
||||
const { getFormState } = useServerFunctions()
|
||||
const { collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
|
||||
|
||||
const { closeModal, openModal } = useModal()
|
||||
const drawerSlug = useDrawerSlug('rich-text-link')
|
||||
const { schemaPath } = useFieldProps()
|
||||
|
||||
const {
|
||||
field: { richTextComponentMap },
|
||||
} = fieldProps
|
||||
const { componentMap } = fieldProps
|
||||
|
||||
const fields = richTextComponentMap.get(linkFieldsSchemaPath)
|
||||
const fields = componentMap[linkFieldsSchemaPath]
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -91,13 +96,17 @@ export const LinkButton: React.FC = () => {
|
||||
}
|
||||
|
||||
const { state } = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
data,
|
||||
operation: 'update',
|
||||
schemaPath: `${schemaPath}.${linkFieldsSchemaPath}`,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
collectionSlug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: [...schemaPath.split('.'), ...linkFieldsSchemaPath.split('.')].join(
|
||||
'.',
|
||||
),
|
||||
})
|
||||
|
||||
setInitialState(state)
|
||||
@@ -119,6 +128,7 @@ export const LinkButton: React.FC = () => {
|
||||
closeModal(drawerSlug)
|
||||
}}
|
||||
initialState={initialState}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@@ -6,17 +6,16 @@ import {
|
||||
Button,
|
||||
Popup,
|
||||
Translation,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useDrawerSlug,
|
||||
useLocale,
|
||||
useModal,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import { deepCopyObject, reduceFieldsToValues } from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Editor, Node, Transforms } from 'slate'
|
||||
import { ReactEditor, useSlate } from 'slate-react'
|
||||
|
||||
@@ -65,21 +64,20 @@ export const LinkElement = () => {
|
||||
|
||||
const fieldMapPath = `${schemaPath}.${linkFieldsSchemaPath}`
|
||||
|
||||
const {
|
||||
field: { richTextComponentMap },
|
||||
} = fieldProps
|
||||
const fields = richTextComponentMap.get(linkFieldsSchemaPath)
|
||||
const { componentMap } = fieldProps
|
||||
const fields = componentMap[linkFieldsSchemaPath]
|
||||
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
|
||||
|
||||
const editor = useSlate()
|
||||
const { config } = useConfig()
|
||||
const { user } = useAuth()
|
||||
const { code: locale } = useLocale()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { closeModal, openModal, toggleModal } = useModal()
|
||||
const [renderModal, setRenderModal] = useState(false)
|
||||
const [renderPopup, setRenderPopup] = useState(false)
|
||||
const [initialState, setInitialState] = useState<FormState>({})
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const drawerSlug = useDrawerSlug('rich-text-link')
|
||||
|
||||
@@ -101,13 +99,15 @@ export const LinkElement = () => {
|
||||
}
|
||||
|
||||
const { state } = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
data,
|
||||
operation: 'update',
|
||||
schemaPath: fieldMapPath,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
collectionSlug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: fieldMapPath ?? '',
|
||||
})
|
||||
|
||||
setInitialState(state)
|
||||
@@ -116,7 +116,20 @@ export const LinkElement = () => {
|
||||
if (renderModal) {
|
||||
void awaitInitialState()
|
||||
}
|
||||
}, [renderModal, element, user, locale, t, collectionSlug, config, id, fieldMapPath])
|
||||
}, [
|
||||
renderModal,
|
||||
element,
|
||||
locale,
|
||||
t,
|
||||
collectionSlug,
|
||||
config,
|
||||
id,
|
||||
fieldMapPath,
|
||||
getFormState,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
docPermissions,
|
||||
])
|
||||
|
||||
return (
|
||||
<span className={baseClass} {...attributes}>
|
||||
@@ -135,6 +148,7 @@ export const LinkElement = () => {
|
||||
setRenderModal(false)
|
||||
}}
|
||||
initialState={initialState}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
)}
|
||||
<Popup
|
||||
|
||||
@@ -4,17 +4,16 @@ import type { FormProps } from '@payloadcms/ui'
|
||||
|
||||
import {
|
||||
Drawer,
|
||||
EditDepthProvider,
|
||||
Form,
|
||||
FormSubmit,
|
||||
RenderFields,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useFieldProps,
|
||||
useHotkey,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
@@ -29,45 +28,57 @@ export const LinkDrawer: React.FC<Props> = ({
|
||||
fields,
|
||||
handleModalSubmit,
|
||||
initialState,
|
||||
schemaPath,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { schemaPath } = useFieldProps()
|
||||
const fieldMapPath = `${schemaPath}.${linkFieldsSchemaPath}`
|
||||
const { id } = useDocumentInfo()
|
||||
const { config } = useConfig()
|
||||
|
||||
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const { state } = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
id,
|
||||
formState: prevFormState,
|
||||
operation: 'update',
|
||||
schemaPath: fieldMapPath,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
schemaPath: fieldMapPath ?? '',
|
||||
})
|
||||
|
||||
return state
|
||||
},
|
||||
|
||||
[config.routes.api, config.serverURL, fieldMapPath, id],
|
||||
[getFormState, id, collectionSlug, getDocPreferences, docPermissions, globalSlug, fieldMapPath],
|
||||
)
|
||||
|
||||
return (
|
||||
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink')}>
|
||||
<Form
|
||||
beforeSubmit={[onChange]}
|
||||
disableValidationOnSubmit
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleModalSubmit}
|
||||
>
|
||||
<RenderFields fields={fields} forceRender path="" readOnly={false} schemaPath="" />
|
||||
<LinkSubmit />
|
||||
</Form>
|
||||
</Drawer>
|
||||
<EditDepthProvider>
|
||||
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink')}>
|
||||
<Form
|
||||
beforeSubmit={[onChange]}
|
||||
disableValidationOnSubmit
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleModalSubmit}
|
||||
>
|
||||
<RenderFields
|
||||
fields={fields}
|
||||
forceRender
|
||||
parentIndexPath=""
|
||||
parentPath={''}
|
||||
parentSchemaPath=""
|
||||
permissions={docPermissions.fields}
|
||||
readOnly={false}
|
||||
/>
|
||||
<LinkSubmit />
|
||||
</Form>
|
||||
</Drawer>
|
||||
</EditDepthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ export type Props = {
|
||||
readonly handleClose: () => void
|
||||
readonly handleModalSubmit: (fields: FormState, data: Record<string, unknown>) => void
|
||||
readonly initialState?: FormState
|
||||
readonly schemaPath: string
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { FormFieldBase, MappedComponent } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
Button,
|
||||
@@ -27,12 +25,7 @@ const initialParams = {
|
||||
depth: 0,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
richTextComponentMap: Map<string, MappedComponent>
|
||||
} & FormFieldBase
|
||||
|
||||
const RelationshipElementComponent: React.FC<Props> = () => {
|
||||
const RelationshipElementComponent: React.FC = () => {
|
||||
const {
|
||||
attributes,
|
||||
children,
|
||||
@@ -194,7 +187,7 @@ const RelationshipElementComponent: React.FC<Props> = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const RelationshipElement = (props: Props): React.ReactNode => {
|
||||
export const RelationshipElement = (props: any): React.ReactNode => {
|
||||
return (
|
||||
<EnabledRelationshipsCondition {...props}>
|
||||
<RelationshipElementComponent {...props} />
|
||||
|
||||
@@ -6,17 +6,17 @@ import type { ClientCollectionConfig } from 'payload'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
Drawer,
|
||||
EditDepthProvider,
|
||||
Form,
|
||||
FormSubmit,
|
||||
RenderFields,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
useModal,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import { deepCopyObject } from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Transforms } from 'slate'
|
||||
@@ -40,16 +40,16 @@ export const UploadDrawer: React.FC<{
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
const { code: locale } = useLocale()
|
||||
const { user } = useAuth()
|
||||
const { closeModal } = useModal()
|
||||
const { id, collectionSlug } = useDocumentInfo()
|
||||
const { id, collectionSlug, docPermissions, getDocPreferences, globalSlug } = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const [initialState, setInitialState] = useState({})
|
||||
const {
|
||||
field: { richTextComponentMap },
|
||||
} = fieldProps
|
||||
const { componentMap } = fieldProps
|
||||
|
||||
const relatedFieldSchemaPath = `${uploadFieldsSchemaPath}.${relatedCollection.slug}`
|
||||
const fields = richTextComponentMap.get(relatedFieldSchemaPath)
|
||||
const fields = componentMap[relatedFieldSchemaPath]
|
||||
|
||||
const { config } = useConfig()
|
||||
|
||||
@@ -72,15 +72,16 @@ export const UploadDrawer: React.FC<{
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const { state } = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
id,
|
||||
collectionSlug,
|
||||
data,
|
||||
operation: 'update',
|
||||
schemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${relatedCollection.slug}`,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
id,
|
||||
collectionSlug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
doNotAbort: true,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${relatedCollection.slug}`,
|
||||
})
|
||||
|
||||
setInitialState(state)
|
||||
@@ -90,56 +91,72 @@ export const UploadDrawer: React.FC<{
|
||||
}, [
|
||||
config,
|
||||
element?.fields,
|
||||
user,
|
||||
locale,
|
||||
t,
|
||||
collectionSlug,
|
||||
id,
|
||||
schemaPath,
|
||||
relatedCollection.slug,
|
||||
getFormState,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
docPermissions,
|
||||
])
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const { state } = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
id,
|
||||
formState: prevFormState,
|
||||
operation: 'update',
|
||||
schemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${relatedCollection.slug}`,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
docPreferences: await getDocPreferences(),
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation: 'update',
|
||||
schemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${relatedCollection.slug}`,
|
||||
})
|
||||
|
||||
return state
|
||||
},
|
||||
|
||||
[config.routes.api, config.serverURL, relatedCollection.slug, schemaPath, id],
|
||||
[
|
||||
getFormState,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
getDocPreferences,
|
||||
globalSlug,
|
||||
schemaPath,
|
||||
relatedCollection.slug,
|
||||
],
|
||||
)
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
title={t('general:editLabel', {
|
||||
label: getTranslation(relatedCollection.labels.singular, i18n),
|
||||
})}
|
||||
>
|
||||
<Form
|
||||
beforeSubmit={[onChange]}
|
||||
disableValidationOnSubmit
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleUpdateEditData}
|
||||
<EditDepthProvider>
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
title={t('general:editLabel', {
|
||||
label: getTranslation(relatedCollection.labels.singular, i18n),
|
||||
})}
|
||||
>
|
||||
<RenderFields
|
||||
fields={Array.isArray(fields) ? fields : []}
|
||||
path=""
|
||||
readOnly={false}
|
||||
schemaPath=""
|
||||
/>
|
||||
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
|
||||
</Form>
|
||||
</Drawer>
|
||||
<Form
|
||||
beforeSubmit={[onChange]}
|
||||
disableValidationOnSubmit
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleUpdateEditData}
|
||||
>
|
||||
<RenderFields
|
||||
fields={Array.isArray(fields) ? fields : []}
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath=""
|
||||
permissions={{}}
|
||||
readOnly={false}
|
||||
/>
|
||||
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</EditDepthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, FormFieldBase } from 'payload'
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
@@ -32,12 +32,7 @@ const initialParams = {
|
||||
depth: 0,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
richTextComponentMap: Map<string, React.ReactNode>
|
||||
} & FormFieldBase
|
||||
|
||||
const UploadElementComponent: React.FC<{ enabledCollectionSlugs?: string[] } & Props> = ({
|
||||
const UploadElementComponent: React.FC<{ enabledCollectionSlugs?: string[] }> = ({
|
||||
enabledCollectionSlugs,
|
||||
}) => {
|
||||
const {
|
||||
@@ -137,7 +132,7 @@ const UploadElementComponent: React.FC<{ enabledCollectionSlugs?: string[] } & P
|
||||
)
|
||||
|
||||
const relatedFieldSchemaPath = `${uploadFieldsSchemaPath}.${relatedCollection.slug}`
|
||||
const customFieldsMap = fieldProps.field.richTextComponentMap.get(relatedFieldSchemaPath)
|
||||
const customFieldsMap = fieldProps.componentMap[relatedFieldSchemaPath]
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -221,7 +216,7 @@ const UploadElementComponent: React.FC<{ enabledCollectionSlugs?: string[] } & P
|
||||
)
|
||||
}
|
||||
|
||||
export const UploadElement = (props: Props): React.ReactNode => {
|
||||
export const UploadElement = (props: any): React.ReactNode => {
|
||||
return (
|
||||
<EnabledRelationshipsCondition {...props} uploads>
|
||||
<UploadElementComponent {...props} />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { RenderComponent, ShimmerEffect, useClientFunctions, useFieldProps } from '@payloadcms/ui'
|
||||
import { ShimmerEffect, useClientFunctions } from '@payloadcms/ui'
|
||||
import React, { lazy, Suspense, useEffect, useState } from 'react'
|
||||
|
||||
import type { RichTextPlugin, SlateFieldProps } from '../types.js'
|
||||
import type { EnabledFeatures } from './types.js'
|
||||
|
||||
import { SlatePropsProvider } from '../utilities/SlatePropsProvider.js'
|
||||
import { createFeatureMap } from './createFeatureMap.js'
|
||||
|
||||
const RichTextEditor = lazy(() =>
|
||||
@@ -15,16 +16,13 @@ const RichTextEditor = lazy(() =>
|
||||
)
|
||||
|
||||
export const RichTextField: React.FC<SlateFieldProps> = (props) => {
|
||||
const {
|
||||
field: { richTextComponentMap },
|
||||
} = props
|
||||
const { componentMap, schemaPath } = props
|
||||
|
||||
const { schemaPath } = useFieldProps()
|
||||
const clientFunctions = useClientFunctions()
|
||||
const [hasLoadedPlugins, setHasLoadedPlugins] = useState(false)
|
||||
|
||||
const [features] = useState<EnabledFeatures>(() => {
|
||||
return createFeatureMap(richTextComponentMap as any)
|
||||
return createFeatureMap(new Map(Object.entries(componentMap)))
|
||||
})
|
||||
|
||||
const [plugins, setPlugins] = useState<RichTextPlugin[]>([])
|
||||
@@ -48,27 +46,25 @@ export const RichTextField: React.FC<SlateFieldProps> = (props) => {
|
||||
|
||||
if (!hasLoadedPlugins) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SlatePropsProvider schemaPath={schemaPath}>
|
||||
{Array.isArray(features.plugins) &&
|
||||
features.plugins.map((Plugin, i) => {
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<RenderComponent mappedComponent={Plugin} />
|
||||
</React.Fragment>
|
||||
)
|
||||
return <React.Fragment key={i}>{Plugin}</React.Fragment>
|
||||
})}
|
||||
</React.Fragment>
|
||||
</SlatePropsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ShimmerEffect height="35vh" />}>
|
||||
<RichTextEditor
|
||||
{...props}
|
||||
elements={features.elements}
|
||||
leaves={features.leaves}
|
||||
plugins={plugins}
|
||||
/>
|
||||
<SlatePropsProvider schemaPath={schemaPath}>
|
||||
<RichTextEditor
|
||||
{...props}
|
||||
elements={features.elements}
|
||||
leaves={features.leaves}
|
||||
plugins={plugins}
|
||||
/>
|
||||
</SlatePropsProvider>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
203
packages/richtext-slate/src/field/rscEntry.tsx
Normal file
203
packages/richtext-slate/src/field/rscEntry.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import {
|
||||
type ClientComponentProps,
|
||||
type ClientField,
|
||||
createClientFields,
|
||||
deepCopyObjectSimple,
|
||||
type Field,
|
||||
type RichTextFieldClient,
|
||||
type ServerComponentProps,
|
||||
} from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from '../types.js'
|
||||
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||
import { RichTextField } from '../exports/client/index.js'
|
||||
import { elements as elementTypes } from '../field/elements/index.js'
|
||||
import { defaultLeaves as leafTypes } from '../field/leaves/index.js'
|
||||
import { linkFieldsSchemaPath } from './elements/link/shared.js'
|
||||
import { uploadFieldsSchemaPath } from './elements/upload/shared.js'
|
||||
export const RscEntrySlateField: React.FC<
|
||||
{
|
||||
args: AdapterArguments
|
||||
} & ClientComponentProps &
|
||||
ServerComponentProps
|
||||
> = ({
|
||||
args,
|
||||
clientField,
|
||||
forceRender,
|
||||
i18n,
|
||||
indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
payload,
|
||||
readOnly,
|
||||
renderedBlocks,
|
||||
schemaPath,
|
||||
}) => {
|
||||
const componentMap: Map<string, ClientField[] | React.ReactNode> = new Map()
|
||||
|
||||
const clientProps = {
|
||||
schemaPath,
|
||||
}
|
||||
|
||||
;(args?.admin?.leaves || Object.values(leafTypes)).forEach((leaf) => {
|
||||
let leafObject: RichTextCustomLeaf
|
||||
|
||||
if (typeof leaf === 'object' && leaf !== null) {
|
||||
leafObject = leaf
|
||||
} else if (typeof leaf === 'string' && leafTypes[leaf]) {
|
||||
leafObject = leafTypes[leaf]
|
||||
}
|
||||
|
||||
if (leafObject) {
|
||||
const LeafButton = leafObject.Button
|
||||
const LeafComponent = leafObject.Leaf
|
||||
|
||||
componentMap.set(
|
||||
`leaf.button.${leafObject.name}`,
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={LeafButton}
|
||||
importMap={payload.importMap}
|
||||
/>,
|
||||
)
|
||||
|
||||
componentMap.set(
|
||||
`leaf.component.${leafObject.name}`,
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={LeafComponent}
|
||||
importMap={payload.importMap}
|
||||
/>,
|
||||
)
|
||||
|
||||
if (Array.isArray(leafObject.plugins)) {
|
||||
leafObject.plugins.forEach((Plugin, i) => {
|
||||
componentMap.set(
|
||||
`leaf.plugin.${leafObject.name}.${i}`,
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={Plugin}
|
||||
importMap={payload.importMap}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||
let element: RichTextCustomElement
|
||||
|
||||
if (typeof el === 'object' && el !== null) {
|
||||
element = el
|
||||
} else if (typeof el === 'string' && elementTypes[el]) {
|
||||
element = elementTypes[el]
|
||||
}
|
||||
|
||||
if (element) {
|
||||
const ElementButton = element.Button
|
||||
const ElementComponent = element.Element
|
||||
|
||||
if (ElementButton) {
|
||||
componentMap.set(
|
||||
`element.button.${element.name}`,
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={ElementButton}
|
||||
importMap={payload.importMap}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
componentMap.set(
|
||||
`element.component.${element.name}`,
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={ElementComponent}
|
||||
importMap={payload.importMap}
|
||||
/>,
|
||||
)
|
||||
|
||||
if (Array.isArray(element.plugins)) {
|
||||
element.plugins.forEach((Plugin, i) => {
|
||||
componentMap.set(
|
||||
`element.plugin.${element.name}.${i}`,
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={Plugin}
|
||||
importMap={payload.importMap}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
let clientFields = deepCopyObjectSimple(
|
||||
args.admin?.link?.fields,
|
||||
) as unknown as ClientField[]
|
||||
clientFields = createClientFields({
|
||||
clientFields,
|
||||
defaultIDType: payload.config.db.defaultIDType,
|
||||
fields: args.admin?.link?.fields as Field[],
|
||||
i18n,
|
||||
})
|
||||
|
||||
componentMap.set(linkFieldsSchemaPath, clientFields)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'upload': {
|
||||
const uploadEnabledCollections = payload.config.collections.filter(
|
||||
({ admin: { enableRichTextRelationship, hidden }, upload }) => {
|
||||
if (hidden === true) {
|
||||
return false
|
||||
}
|
||||
|
||||
return enableRichTextRelationship && Boolean(upload) === true
|
||||
},
|
||||
)
|
||||
|
||||
uploadEnabledCollections.forEach((collection) => {
|
||||
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
|
||||
let clientFields = deepCopyObjectSimple(
|
||||
args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
) as unknown as ClientField[]
|
||||
clientFields = createClientFields({
|
||||
clientFields,
|
||||
defaultIDType: payload.config.db.defaultIDType,
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
i18n,
|
||||
})
|
||||
|
||||
componentMap.set(`${uploadFieldsSchemaPath}.${collection.slug}`, clientFields)
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<RichTextField
|
||||
componentMap={Object.fromEntries(componentMap)}
|
||||
field={clientField as RichTextFieldClient}
|
||||
forceRender={forceRender}
|
||||
indexPath={indexPath}
|
||||
parentPath={parentPath}
|
||||
parentSchemaPath={parentSchemaPath}
|
||||
path={path}
|
||||
readOnly={readOnly}
|
||||
renderedBlocks={renderedBlocks}
|
||||
schemaPath={schemaPath}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +1,21 @@
|
||||
import type { MappedComponent } from 'payload'
|
||||
|
||||
import type { RichTextPlugin, SlateFieldProps } from '../types.js'
|
||||
|
||||
export type EnabledFeatures = {
|
||||
elements: {
|
||||
[name: string]: {
|
||||
Button: MappedComponent
|
||||
Element: MappedComponent
|
||||
Button: React.ReactNode
|
||||
Element: React.ReactNode
|
||||
name: string
|
||||
}
|
||||
}
|
||||
leaves: {
|
||||
[name: string]: {
|
||||
Button: MappedComponent
|
||||
Leaf: MappedComponent
|
||||
Button: React.ReactNode
|
||||
Leaf: React.ReactNode
|
||||
name: string
|
||||
}
|
||||
}
|
||||
plugins: MappedComponent[]
|
||||
plugins: React.ReactNode[]
|
||||
}
|
||||
|
||||
export type LoadedSlateFieldProps = {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import type { ClientField, Field, MappedComponent, RichTextGenerateComponentMap } from 'payload'
|
||||
|
||||
import { createClientFields } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { deepCopyObjectSimple } from 'payload'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from './types.js'
|
||||
|
||||
import { elements as elementTypes } from './field/elements/index.js'
|
||||
import { linkFieldsSchemaPath } from './field/elements/link/shared.js'
|
||||
import { uploadFieldsSchemaPath } from './field/elements/upload/shared.js'
|
||||
import { defaultLeaves as leafTypes } from './field/leaves/index.js'
|
||||
|
||||
export const getGenerateComponentMap =
|
||||
(args: AdapterArguments): RichTextGenerateComponentMap =>
|
||||
({ createMappedComponent, i18n, importMap, payload }) => {
|
||||
const componentMap: Map<string, ClientField[] | MappedComponent> = new Map()
|
||||
|
||||
;(args?.admin?.leaves || Object.values(leafTypes)).forEach((leaf) => {
|
||||
let leafObject: RichTextCustomLeaf
|
||||
|
||||
if (typeof leaf === 'object' && leaf !== null) {
|
||||
leafObject = leaf
|
||||
} else if (typeof leaf === 'string' && leafTypes[leaf]) {
|
||||
leafObject = leafTypes[leaf]
|
||||
}
|
||||
|
||||
if (leafObject) {
|
||||
const LeafButton = leafObject.Button
|
||||
const LeafComponent = leafObject.Leaf
|
||||
|
||||
componentMap.set(
|
||||
`leaf.button.${leafObject.name}`,
|
||||
createMappedComponent(LeafButton, undefined, undefined, 'slate-LeafButton'),
|
||||
)
|
||||
|
||||
componentMap.set(
|
||||
`leaf.component.${leafObject.name}`,
|
||||
createMappedComponent(LeafComponent, undefined, undefined, 'slate-LeafComponent'),
|
||||
)
|
||||
|
||||
if (Array.isArray(leafObject.plugins)) {
|
||||
leafObject.plugins.forEach((Plugin, i) => {
|
||||
componentMap.set(
|
||||
`leaf.plugin.${leafObject.name}.${i}`,
|
||||
createMappedComponent(Plugin, undefined, undefined, 'slate-LeafPlugin'),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||
let element: RichTextCustomElement
|
||||
|
||||
if (typeof el === 'object' && el !== null) {
|
||||
element = el
|
||||
} else if (typeof el === 'string' && elementTypes[el]) {
|
||||
element = elementTypes[el]
|
||||
}
|
||||
|
||||
if (element) {
|
||||
const ElementButton = element.Button
|
||||
const ElementComponent = element.Element
|
||||
|
||||
if (ElementButton) {
|
||||
componentMap.set(
|
||||
`element.button.${element.name}`,
|
||||
createMappedComponent(ElementButton, undefined, undefined, 'slate-ElementButton'),
|
||||
)
|
||||
}
|
||||
componentMap.set(
|
||||
`element.component.${element.name}`,
|
||||
createMappedComponent(ElementComponent, undefined, undefined, 'slate-ElementComponent'),
|
||||
)
|
||||
|
||||
if (Array.isArray(element.plugins)) {
|
||||
element.plugins.forEach((Plugin, i) => {
|
||||
componentMap.set(
|
||||
`element.plugin.${element.name}.${i}`,
|
||||
createMappedComponent(Plugin, undefined, undefined, 'slate-ElementPlugin'),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
let clientFields = deepCopyObjectSimple(
|
||||
args.admin?.link?.fields,
|
||||
) as unknown as ClientField[]
|
||||
clientFields = createClientFields({
|
||||
clientFields,
|
||||
createMappedComponent,
|
||||
fields: args.admin?.link?.fields as Field[],
|
||||
i18n,
|
||||
importMap,
|
||||
payload,
|
||||
})
|
||||
|
||||
componentMap.set(linkFieldsSchemaPath, clientFields)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'upload': {
|
||||
const uploadEnabledCollections = payload.config.collections.filter(
|
||||
({ admin: { enableRichTextRelationship, hidden }, upload }) => {
|
||||
if (hidden === true) {
|
||||
return false
|
||||
}
|
||||
|
||||
return enableRichTextRelationship && Boolean(upload) === true
|
||||
},
|
||||
)
|
||||
|
||||
uploadEnabledCollections.forEach((collection) => {
|
||||
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
|
||||
let clientFields = deepCopyObjectSimple(
|
||||
args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
) as unknown as ClientField[]
|
||||
clientFields = createClientFields({
|
||||
clientFields,
|
||||
createMappedComponent,
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
i18n,
|
||||
importMap,
|
||||
payload,
|
||||
})
|
||||
|
||||
componentMap.set(`${uploadFieldsSchemaPath}.${collection.slug}`, clientFields)
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'relationship':
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return componentMap
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Field, RichTextAdapter } from 'payload'
|
||||
|
||||
import { traverseFields } from '@payloadcms/ui/utilities/buildFieldSchemaMap/traverseFields'
|
||||
|
||||
import type { AdapterArguments, RichTextCustomElement } from './types.js'
|
||||
|
||||
import { elements as elementTypes } from './field/elements/index.js'
|
||||
@@ -8,7 +10,7 @@ import { uploadFieldsSchemaPath } from './field/elements/upload/shared.js'
|
||||
|
||||
export const getGenerateSchemaMap =
|
||||
(args: AdapterArguments): RichTextAdapter['generateSchemaMap'] =>
|
||||
({ config, schemaMap, schemaPath }) => {
|
||||
({ config, i18n, schemaMap, schemaPath }) => {
|
||||
;(args?.admin?.elements || Object.values(elementTypes)).forEach((el) => {
|
||||
let element: RichTextCustomElement
|
||||
|
||||
@@ -21,10 +23,21 @@ export const getGenerateSchemaMap =
|
||||
if (element) {
|
||||
switch (element.name) {
|
||||
case 'link': {
|
||||
schemaMap.set(
|
||||
`${schemaPath}.${linkFieldsSchemaPath}`,
|
||||
args.admin?.link?.fields as Field[],
|
||||
)
|
||||
if (args.admin?.link?.fields) {
|
||||
schemaMap.set(`${schemaPath}.${linkFieldsSchemaPath}`, {
|
||||
fields: args.admin?.link?.fields as Field[],
|
||||
})
|
||||
|
||||
// generate schema map entries for sub-fields using traverseFields
|
||||
traverseFields({
|
||||
config,
|
||||
fields: args.admin?.link?.fields as Field[],
|
||||
i18n,
|
||||
parentIndexPath: '',
|
||||
parentSchemaPath: `${schemaPath}.${linkFieldsSchemaPath}`,
|
||||
schemaMap,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
@@ -42,10 +55,19 @@ export const getGenerateSchemaMap =
|
||||
|
||||
uploadEnabledCollections.forEach((collection) => {
|
||||
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
|
||||
schemaMap.set(
|
||||
`${schemaPath}.${uploadFieldsSchemaPath}.${collection.slug}`,
|
||||
args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
)
|
||||
schemaMap.set(`${schemaPath}.${uploadFieldsSchemaPath}.${collection.slug}`, {
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
})
|
||||
|
||||
// generate schema map entries for sub-fields using traverseFields
|
||||
traverseFields({
|
||||
config,
|
||||
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
|
||||
i18n,
|
||||
parentIndexPath: '',
|
||||
parentSchemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${collection.slug}`,
|
||||
schemaMap,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -47,16 +47,16 @@ export function slateEditor(
|
||||
}
|
||||
|
||||
return {
|
||||
CellComponent: '@payloadcms/richtext-slate/client#RichTextCell',
|
||||
FieldComponent: '@payloadcms/richtext-slate/client#RichTextField',
|
||||
generateComponentMap: {
|
||||
path: '@payloadcms/richtext-slate/generateComponentMap#getGenerateComponentMap',
|
||||
serverProps: args,
|
||||
CellComponent: '@payloadcms/richtext-slate/rsc#RscEntrySlateCell',
|
||||
FieldComponent: {
|
||||
path: '@payloadcms/richtext-slate/rsc#RscEntrySlateField',
|
||||
serverProps: {
|
||||
args,
|
||||
},
|
||||
},
|
||||
generateImportMap: ({ addToImportMap }) => {
|
||||
addToImportMap('@payloadcms/richtext-slate/client#RichTextCell')
|
||||
addToImportMap('@payloadcms/richtext-slate/client#RichTextField')
|
||||
addToImportMap('@payloadcms/richtext-slate/generateComponentMap#getGenerateComponentMap')
|
||||
addToImportMap('@payloadcms/richtext-slate/rsc#RscEntrySlateCell')
|
||||
addToImportMap('@payloadcms/richtext-slate/rsc#RscEntrySlateField')
|
||||
Object.values(leafTypes).forEach((leaf) => {
|
||||
if (leaf.Button) {
|
||||
addToImportMap(leaf.Button)
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { Field, PayloadComponent, RichTextFieldClientProps, SanitizedConfig } from 'payload'
|
||||
import type {
|
||||
ClientField,
|
||||
Field,
|
||||
PayloadComponent,
|
||||
RichTextFieldClientProps,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
import type { Editor } from 'slate'
|
||||
|
||||
export type TextNode = { [x: string]: unknown; text: string }
|
||||
@@ -70,4 +76,8 @@ export type AdapterArguments = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SlateFieldProps = RichTextFieldClientProps<any[], AdapterArguments, AdapterArguments>
|
||||
export type SlateFieldProps = {
|
||||
componentMap: {
|
||||
[x: string]: ClientField[] | React.ReactNode
|
||||
}
|
||||
} & RichTextFieldClientProps<any[], AdapterArguments, AdapterArguments>
|
||||
|
||||
27
packages/richtext-slate/src/utilities/SlatePropsProvider.tsx
Normal file
27
packages/richtext-slate/src/utilities/SlatePropsProvider.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, type ReactNode, useContext } from 'react'
|
||||
|
||||
interface SlateProps {
|
||||
schemaPath: string
|
||||
}
|
||||
|
||||
const SlatePropsContext = createContext<SlateProps | undefined>(undefined)
|
||||
|
||||
export function SlatePropsProvider({
|
||||
children,
|
||||
schemaPath,
|
||||
}: {
|
||||
children: ReactNode
|
||||
schemaPath: string
|
||||
}) {
|
||||
return <SlatePropsContext.Provider value={{ schemaPath }}>{children}</SlatePropsContext.Provider>
|
||||
}
|
||||
|
||||
export function useSlateProps() {
|
||||
const context = useContext(SlatePropsContext)
|
||||
if (!context) {
|
||||
throw new Error('useSlateProps must be used within SlatePropsProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Editor } from 'slate'
|
||||
|
||||
import { useAddClientFunction, useFieldProps } from '@payloadcms/ui'
|
||||
import { useAddClientFunction } from '@payloadcms/ui'
|
||||
|
||||
import { useSlateProps } from './SlatePropsProvider.js'
|
||||
|
||||
type Plugin = (editor: Editor) => Editor
|
||||
|
||||
export const useSlatePlugin = (key: string, plugin: Plugin) => {
|
||||
const { schemaPath } = useFieldProps()
|
||||
const { schemaPath } = useSlateProps()
|
||||
|
||||
useAddClientFunction(`slatePlugin.${schemaPath}.${key}`, plugin)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user