Merge branch 'feat/next-poc' of https://github.com/payloadcms/payload into feat/next-poc

This commit is contained in:
Jarrod Flesch
2024-02-27 12:33:10 -05:00
23 changed files with 442 additions and 242 deletions

View File

@@ -2,7 +2,7 @@
import type { CellComponentProps, CellProps } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import { formatDocTitle, useConfig, useIntersect, useTranslation } from '@payloadcms/ui'
import { canUseDOM, formatDocTitle, useConfig, useIntersect, useTranslation } from '@payloadcms/ui'
import React, { useEffect, useState } from 'react'
import { useListRelationships } from '../../../RelationshipProvider'
@@ -30,7 +30,7 @@ export const RelationshipCell: React.FC<RelationshipCellProps> = ({
const [hasRequested, setHasRequested] = useState(false)
const { i18n, t } = useTranslation()
const isAboveViewport = entry?.boundingClientRect?.top < window.innerHeight
const isAboveViewport = canUseDOM ? entry?.boundingClientRect?.top < window.innerHeight : false
useEffect(() => {
if (cellData && isAboveViewport && !hasRequested) {

View File

@@ -2,10 +2,11 @@
import Link from 'next/link'
import React from 'react' // TODO: abstract this out to support all routers
import type { CellComponentProps, CellProps } from 'payload/types'
import type { CellProps } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import { useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
import { TableCellProvider } from '@payloadcms/ui'
import cellComponents from './fields'
import { CodeCell } from './fields/Code'
@@ -19,6 +20,7 @@ export const DefaultCell: React.FC<CellProps> = (props) => {
isFieldAffectingData,
label,
onClick: onClickFromProps,
richTextComponentMap,
} = props
const { i18n } = useTranslation()
@@ -77,12 +79,35 @@ export const DefaultCell: React.FC<CellProps> = (props) => {
)
}
let CellComponent: React.FC<CellComponentProps> =
cellData && (CellComponentOverride ? CellComponentOverride : cellComponents[fieldType])
const DefaultCellComponent = cellComponents[fieldType]
if (!CellComponent) {
let CellComponent: React.ReactNode =
cellData &&
(CellComponentOverride ? ( // CellComponentOverride is used for richText
<TableCellProvider richTextComponentMap={richTextComponentMap}>
{CellComponentOverride}
</TableCellProvider>
) : null)
if (!CellComponent && DefaultCellComponent) {
CellComponent = (
<DefaultCellComponent
cellData={cellData}
customCellContext={customCellContext}
rowData={rowData}
/>
)
} else if (!CellComponent && !DefaultCellComponent) {
// DefaultCellComponent does not exist for certain field types like `text`
if (customCellContext.uploadConfig && isFieldAffectingData && name === 'filename') {
CellComponent = cellComponents.File
const FileCellComponent = cellComponents.File
CellComponent = (
<FileCellComponent
cellData={cellData}
customCellContext={customCellContext}
rowData={rowData}
/>
)
} else {
return (
<WrapElement {...wrapElementProps}>
@@ -99,9 +124,5 @@ export const DefaultCell: React.FC<CellProps> = (props) => {
}
}
return (
<WrapElement {...wrapElementProps}>
<CellComponent cellData={cellData} customCellContext={customCellContext} rowData={rowData} />
</WrapElement>
)
return <WrapElement {...wrapElementProps}>{CellComponent}</WrapElement>
}

View File

@@ -31,7 +31,7 @@ type RichTextAdapterBase<
config: SanitizedConfig
schemaPath: string
}) => Map<string, React.ReactNode>
generateSchemaMap: (args: {
generateSchemaMap?: (args: {
config: SanitizedConfig
schemaMap: Map<string, Field[]>
schemaPath: string

View File

@@ -16,7 +16,7 @@ export type CellProps = {
*
* This is used to provide the RichText cell component for the RichText field.
*/
CellComponentOverride?: React.ComponentType<CellComponentProps>
CellComponentOverride?: React.ReactNode
blocks?: {
labels: BlockField['labels']
slug: string
@@ -36,6 +36,7 @@ export type CellProps = {
}) => void
options?: SelectField['options']
relationTo?: RelationshipField['relationTo']
richTextComponentMap?: Map<string, React.ReactNode> // any should be MappedField
}
export type CellComponentProps<Data = unknown> = {
@@ -44,5 +45,6 @@ export type CellComponentProps<Data = unknown> = {
collectionSlug?: SanitizedCollectionConfig['slug']
uploadConfig?: SanitizedCollectionConfig['upload']
}
richTextComponentMap?: Map<string, React.ReactNode>
rowData?: Record<string, unknown>
}

View File

@@ -49,7 +49,9 @@
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.4.0"
"payload": "^2.4.0",
"@payloadcms/translations": "workspace:^",
"@payloadcms/ui": "workspace:^"
},
"exports": {
".": {

View File

@@ -1,35 +1,45 @@
'use client'
import type { SerializedEditorState } from 'lexical'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { CellComponentProps, RichTextField } from 'payload/types'
import { createHeadlessEditor } from '@lexical/headless'
import { useTableCell } from '@payloadcms/ui/elements'
import { $getRoot } from 'lexical'
import React, { useEffect } from 'react'
import type { AdapterProps } from '../types'
import type { SanitizedEditorConfig } from '../field/lexical/config/types'
import { useFieldPath } from '../../../ui/src/forms/FieldPathProvider'
import { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
import { defaultEditorLexicalConfig } from '../field/lexical/config/defaultClient'
import { sanitizeEditorConfig } from '../field/lexical/config/sanitize'
import { getEnabledNodes } from '../field/lexical/nodes'
export const RichTextCell: React.FC<
CellComponentProps<
RichTextField<SerializedEditorState, AdapterProps, AdapterProps>,
SerializedEditorState
> &
AdapterProps
> = ({ data, editorConfig }) => {
export const RichTextCell: React.FC<{
lexicalEditorConfig: LexicalEditorConfig
}> = ({ lexicalEditorConfig }) => {
const [preview, setPreview] = React.useState('Loading...')
const { schemaPath } = useFieldPath()
const clientFunctions = useClientFunctions()
const { cellData, cellProps, columnIndex, richTextComponentMap, rowData } = useTableCell()
useEffect(() => {
let dataToUse = data
let dataToUse = cellData
if (dataToUse == null) {
setPreview('')
return
}
const finalSanitizedEditorConfig: SanitizedEditorConfig = sanitizeEditorConfig({
features: [],
lexical: lexicalEditorConfig
? () => Promise.resolve(lexicalEditorConfig)
: () => Promise.resolve(defaultEditorLexicalConfig),
})
// Transform data through load hooks
if (editorConfig?.features?.hooks?.load?.length) {
editorConfig.features.hooks.load.forEach((hook) => {
if (finalSanitizedEditorConfig?.features?.hooks?.load?.length) {
finalSanitizedEditorConfig.features.hooks.load.forEach((hook) => {
dataToUse = hook({ incomingEditorState: dataToUse })
})
}
@@ -51,11 +61,11 @@ export const RichTextCell: React.FC<
return
}
editorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
void finalSanitizedEditorConfig.lexical().then((lexicalConfig: LexicalEditorConfig) => {
// initialize headless editor
const headlessEditor = createHeadlessEditor({
namespace: lexicalConfig.namespace,
nodes: getEnabledNodes({ editorConfig }),
nodes: getEnabledNodes({ editorConfig: finalSanitizedEditorConfig }),
theme: lexicalConfig.theme,
})
headlessEditor.setEditorState(headlessEditor.parseEditorState(dataToUse))
@@ -68,7 +78,7 @@ export const RichTextCell: React.FC<
// Limiting the number of characters shown is done in a CSS rule
setPreview(textContent)
})
}, [data, editorConfig])
}, [cellData, lexicalEditorConfig])
return <span>{preview}</span>
}

View File

@@ -1,11 +1,11 @@
'use client'
import type { SerializedEditorState } from 'lexical'
import { Error, FieldDescription, Label, useField } from '@payloadcms/ui'
import { type FormFieldBase, useField } from '@payloadcms/ui'
import React, { useCallback } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import type { FieldProps } from '../types'
import type { SanitizedEditorConfig } from './lexical/config/types'
import { richTextValidateHOC } from '../validate'
import './index.scss'
@@ -13,26 +13,42 @@ import { LexicalProvider } from './lexical/LexicalProvider'
const baseClass = 'rich-text-lexical'
const RichText: React.FC<FieldProps> = (props) => {
const RichText: React.FC<
FormFieldBase & {
editorConfig: SanitizedEditorConfig // With rendered features n stuff
name: string
richTextComponentMap: Map<string, React.ReactNode>
}
> = (props) => {
const {
name,
admin: { className, condition, description, readOnly, style, width } = {
className,
condition,
description,
readOnly,
style,
width,
},
AfterInput,
BeforeInput,
Description,
Error,
Label,
className,
docPreferences,
editorConfig,
fieldMap,
initialSubfieldState,
label,
locale,
localized,
maxLength,
minLength,
path: pathFromProps,
placeholder,
readOnly,
required,
richTextComponentMap,
rtl,
style,
user,
validate = richTextValidateHOC({ editorConfig }),
width,
} = props
const path = pathFromProps || name
const memoizedValidate = useCallback(
(value, validationOptions) => {
return validate(value, { ...validationOptions, props, required })
@@ -44,12 +60,11 @@ const RichText: React.FC<FieldProps> = (props) => {
)
const fieldType = useField<SerializedEditorState>({
// condition,
path,
path: pathFromProps || name,
validate: memoizedValidate,
})
const { errorMessage, initialValue, setValue, showError, value } = fieldType
const { errorMessage, initialValue, path, schemaPath, setValue, showError, value } = fieldType
const classes = [
baseClass,
@@ -71,8 +86,8 @@ const RichText: React.FC<FieldProps> = (props) => {
}}
>
<div className={`${baseClass}__wrap`}>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
{Error}
{Label}
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider
editorConfig={editorConfig}
@@ -95,13 +110,13 @@ const RichText: React.FC<FieldProps> = (props) => {
value={value}
/>
</ErrorBoundary>
<FieldDescription description={description} path={path} value={value} />
{Description}
</div>
</div>
)
}
function fallbackRender({ error }): JSX.Element {
function fallbackRender({ error }): React.ReactElement {
// Call resetErrorBoundary() to reset the error boundary and retry the render.
return (

View File

@@ -0,0 +1,11 @@
'use client'
import type React from 'react'
import { useLexicalFeature } from '../../../useLexicalFeature'
import { ParagraphFeature, key } from './index'
export const ParagraphFeatureComponent: React.FC = () => {
useLexicalFeature(key, ParagraphFeature)
return null
}

View File

@@ -5,9 +5,13 @@ import type { FeatureProvider } from '../types'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { ParagraphFeatureComponent } from './Component'
export const key = 'paragraph' as const
export const ParagraphFeature = (): FeatureProvider => {
return {
Component: ParagraphFeatureComponent,
feature: () => {
return {
floatingSelectToolbar: {
@@ -57,6 +61,6 @@ export const ParagraphFeature = (): FeatureProvider => {
},
}
},
key: 'paragraph',
key: key,
}
}

View File

@@ -15,7 +15,7 @@ import * as React from 'react'
// @ts-expect-error-next-line TypeScript being dumb
const RawUploadComponent = React.lazy(async () => await import('../component'))
export interface RawUploadPayload {
export type RawUploadPayload = {
fields: {
// unknown, custom fields:
[key: string]: unknown
@@ -104,7 +104,6 @@ export class UploadNode extends DecoratorBlockNode {
}
decorate(): JSX.Element {
// @ts-expect-error-next-line
return <RawUploadComponent data={this.__data} format={this.__format} nodeKey={this.getKey()} />
}

View File

@@ -26,7 +26,7 @@ export const LexicalPluginToLexicalFeature = (props?: Props): FeatureProvider =>
: (props?.converters as LexicalPluginNodeConverter[]) || defaultConverters
return {
feature: ({ resolvedFeatures, unsanitizedEditorConfig }) => {
feature: ({ resolvedFeatures, unSanitizedEditorConfig }) => {
return {
hooks: {
load({ incomingEditorState }) {

View File

@@ -154,27 +154,31 @@ export type Feature = {
}
export type FeatureProvider = {
Component: React.FC
/** Keys of dependencies needed for this feature. These dependencies do not have to be loaded first */
dependencies?: string[]
/** Keys of priority dependencies needed for this feature. These dependencies have to be loaded first and are available in the `feature` property*/
dependenciesPriority?: string[]
/** Keys of soft-dependencies needed for this feature. These dependencies are optional, but are considered as last-priority in the loading process */
dependenciesSoft?: string[]
feature: (props: {
/** unsanitizedEditorConfig.features, but mapped */
/** unSanitizedEditorConfig.features, but mapped */
featureProviderMap: FeatureProviderMap
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
resolvedFeatures: ResolvedFeatureMap
// unsanitized EditorConfig,
unsanitizedEditorConfig: EditorConfig
unSanitizedEditorConfig: EditorConfig
}) => Feature
key: string
}
export type ResolvedFeature = Feature &
Required<
Pick<FeatureProvider, 'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key'>
Pick<
FeatureProvider,
'Component' | 'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key'
>
>
export type ResolvedFeatureMap = Map<string, ResolvedFeature>

View File

@@ -1,16 +1,78 @@
'use client'
import { ShimmerEffect } from '@payloadcms/ui'
import React, { Suspense, lazy } from 'react'
import type { EditorConfig as LexicalEditorConfig } from 'lexical/LexicalEditor'
import type { FieldProps } from '../types'
import { type FormFieldBase, ShimmerEffect } from '@payloadcms/ui'
import React, { Suspense, lazy, useEffect, useState } from 'react'
import type { SanitizedEditorConfig } from './lexical/config/types'
import { useFieldPath } from '../../../ui/src/forms/FieldPathProvider'
import { useClientFunctions } from '../../../ui/src/providers/ClientFunction'
import { defaultEditorLexicalConfig } from './lexical/config/defaultClient'
import { sanitizeEditorConfig } from './lexical/config/sanitize'
// @ts-expect-error-next-line Just TypeScript being broken // TODO: Open TypeScript issue
const RichTextEditor = lazy(() => import('./Field'))
export const RichTextField: React.FC<FieldProps> = (props) => {
export const RichTextField: React.FC<
FormFieldBase & {
lexicalEditorConfig: LexicalEditorConfig
name: string
richTextComponentMap: Map<string, React.ReactNode>
}
> = (props) => {
const { lexicalEditorConfig, richTextComponentMap } = props
const { schemaPath } = useFieldPath()
const clientFunctions = useClientFunctions()
const finalSanitizedEditorConfig: SanitizedEditorConfig = sanitizeEditorConfig({
features: [],
lexical: lexicalEditorConfig
? () => Promise.resolve(lexicalEditorConfig)
: () => Promise.resolve(defaultEditorLexicalConfig),
})
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
const [featureComponents, setFeatureComponents] = useState<React.ReactNode>([])
const featureProviders = Array.from(richTextComponentMap.values())
useEffect(() => {
if (!hasLoadedFeatures) {
const featureComponentsLocal: React.ReactNode[] = []
Object.entries(clientFunctions).forEach(([key, plugin]) => {
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
featureComponentsLocal.push(plugin)
}
})
if (featureComponentsLocal.length === featureProviders.length) {
setFeatureComponents(featureComponentsLocal)
setHasLoadedFeatures(true)
}
}
}, [hasLoadedFeatures, clientFunctions, schemaPath, featureProviders.length])
if (!hasLoadedFeatures) {
return (
<React.Fragment>
{Array.isArray(featureProviders) &&
featureProviders.map((FeatureProvider, i) => {
return <React.Fragment key={i}>{FeatureProvider}</React.Fragment>
})}
</React.Fragment>
)
}
const features = clientFunctions
console.log('clientFunctions', features['lexicalFeature.posts.richText.paragraph'])
return (
<Suspense fallback={<ShimmerEffect height="35vh" />}>
<RichTextEditor {...props} />
<RichTextEditor {...props} editorConfig={finalSanitizedEditorConfig} />
</Suspense>
)
}

View File

@@ -96,12 +96,12 @@ export function sortFeaturesForOptimalLoading(
}
export function loadFeatures({
unsanitizedEditorConfig,
unSanitizedEditorConfig,
}: {
unsanitizedEditorConfig: EditorConfig
unSanitizedEditorConfig: EditorConfig
}): ResolvedFeatureMap {
// First remove all duplicate features. The LAST feature with a given key wins.
unsanitizedEditorConfig.features = unsanitizedEditorConfig.features
unSanitizedEditorConfig.features = unSanitizedEditorConfig.features
.reverse()
.filter((f, i, arr) => {
const firstIndex = arr.findIndex((f2) => f2.key === f.key)
@@ -110,15 +110,15 @@ export function loadFeatures({
.reverse()
const featureProviderMap: FeatureProviderMap = new Map(
unsanitizedEditorConfig.features.map((f) => [f.key, f] as [string, FeatureProvider]),
unSanitizedEditorConfig.features.map((f) => [f.key, f] as [string, FeatureProvider]),
)
unsanitizedEditorConfig.features = sortFeaturesForOptimalLoading(unsanitizedEditorConfig.features)
unSanitizedEditorConfig.features = sortFeaturesForOptimalLoading(unSanitizedEditorConfig.features)
const resolvedFeatures: ResolvedFeatureMap = new Map()
// Make sure all dependencies declared in the respective features exist
for (const featureProvider of unsanitizedEditorConfig.features) {
for (const featureProvider of unSanitizedEditorConfig.features) {
if (!featureProvider.key) {
throw new Error(
`A Feature you've added does not have a key. Please add a key to the feature. This is used to uniquely identify the feature.`,
@@ -126,7 +126,7 @@ export function loadFeatures({
}
if (featureProvider.dependencies?.length) {
for (const dependencyKey of featureProvider.dependencies) {
const found = unsanitizedEditorConfig.features.find((f) => f.key === dependencyKey)
const found = unSanitizedEditorConfig.features.find((f) => f.key === dependencyKey)
if (!found) {
throw new Error(
`Feature ${featureProvider.key} has a dependency ${dependencyKey} which does not exist.`,
@@ -140,7 +140,7 @@ export function loadFeatures({
// look in the resolved features instead of the editorConfig.features, as a dependency requires the feature to be loaded before it, contrary to a soft-dependency
const found = resolvedFeatures.get(priorityDependencyKey)
if (!found) {
const existsInEditorConfig = unsanitizedEditorConfig.features.find(
const existsInEditorConfig = unSanitizedEditorConfig.features.find(
(f) => f.key === priorityDependencyKey,
)
if (!existsInEditorConfig) {
@@ -159,10 +159,11 @@ export function loadFeatures({
const feature = featureProvider.feature({
featureProviderMap,
resolvedFeatures,
unsanitizedEditorConfig,
unSanitizedEditorConfig,
})
resolvedFeatures.set(featureProvider.key, {
...feature,
Component: featureProvider.Component,
dependencies: featureProvider.dependencies,
dependenciesPriority: featureProvider.dependenciesPriority,
dependenciesSoft: featureProvider.dependenciesSoft,

View File

@@ -171,7 +171,7 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
export function sanitizeEditorConfig(editorConfig: EditorConfig): SanitizedEditorConfig {
const resolvedFeatureMap = loadFeatures({
unsanitizedEditorConfig: editorConfig,
unSanitizedEditorConfig: editorConfig,
})
return {

View File

@@ -0,0 +1,23 @@
import type { RichTextAdapter } from 'payload/types'
import React from 'react'
import type { ResolvedFeatureMap } from './field/features/types'
export const getGenerateComponentMap =
(args: { resolvedFeatureMap: ResolvedFeatureMap }): RichTextAdapter['generateComponentMap'] =>
({ config }) => {
const componentMap = new Map()
console.log('args.resolvedFeatureMap', args.resolvedFeatureMap)
for (const key of args.resolvedFeatureMap.keys()) {
console.log('key', key)
const resolvedFeature = args.resolvedFeatureMap.get(key)
const Component = resolvedFeature.Component
componentMap.set(`feature.${key}`, <Component />)
}
console.log('componentMaaap', componentMap)
return componentMap
}

View File

@@ -5,7 +5,7 @@ import type { RichTextAdapter } from 'payload/types'
import { withNullableJSONSchemaType } from 'payload/utilities'
import type { FeatureProvider } from './field/features/types'
import type { FeatureProvider, ResolvedFeatureMap } from './field/features/types'
import type { EditorConfig, SanitizedEditorConfig } from './field/lexical/config/types'
import type { AdapterProps } from './types'
@@ -14,8 +14,10 @@ import {
defaultEditorFeatures,
defaultSanitizedEditorConfig,
} from './field/lexical/config/default'
import { sanitizeEditorConfig } from './field/lexical/config/sanitize'
import { loadFeatures } from './field/lexical/config/loader'
import { sanitizeFeatures } from './field/lexical/config/sanitize'
import { cloneDeep } from './field/lexical/utils/cloneDeep'
import { getGenerateComponentMap } from './generateComponentMap'
import { richTextRelationshipPromise } from './populate/richTextRelationshipPromise'
import { richTextValidateHOC } from './validate'
@@ -31,9 +33,12 @@ export type LexicalRichTextAdapter = RichTextAdapter<SerializedEditorState, Adap
}
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapter {
let finalSanitizedEditorConfig: SanitizedEditorConfig
let resolvedFeatureMap: ResolvedFeatureMap = null // For client and sending to client. Better than serializing completely on client. That way the feature loading can be done on the server.
let finalSanitizedEditorConfig: SanitizedEditorConfig // For server only
if (!props || (!props.features && !props.lexical)) {
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedEditorConfig)
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
} else {
let features: FeatureProvider[] =
props.features && typeof props.features === 'function'
@@ -45,169 +50,177 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
const lexical: LexicalEditorConfig = props.lexical
finalSanitizedEditorConfig = sanitizeEditorConfig({
features,
lexical: props.lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
resolvedFeatureMap = loadFeatures({
unSanitizedEditorConfig: {
features,
lexical: lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
},
})
finalSanitizedEditorConfig = {
features: sanitizeFeatures(resolvedFeatureMap),
lexical: lexical ? () => Promise.resolve(lexical) : defaultEditorConfig.lexical,
resolvedFeatureMap: resolvedFeatureMap,
}
}
// TODO: re-implement this once migrated to RSC
return null
return {
LazyCellComponent: () =>
// @ts-expect-error
import('./cell').then((module) => {
const RichTextCell = module.RichTextCell
return import('@payloadcms/ui').then((module2) =>
module2.withMergedProps({
Component: RichTextCell,
toMergeIntoProps: { lexicalEditorConfig: props.lexical }, // lexicalEditorConfig is serializable
}),
)
}),
// return {
// LazyCellComponent: () =>
// // @ts-ignore-next-line
// import('./cell').then((module) => {
// const RichTextCell = module.RichTextCell
// return import('@payloadcms/ui').then((module2) =>
// module2.withMergedProps({
// Component: RichTextCell,
// toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
// }),
// )
// }),
LazyFieldComponent: () =>
// @ts-expect-error
import('./field').then((module) => {
const RichTextField = module.RichTextField
return import('@payloadcms/ui').then((module2) =>
module2.withMergedProps({
Component: RichTextField,
toMergeIntoProps: { lexicalEditorConfig: props.lexical }, // lexicalEditorConfig is serializable
}),
)
}),
afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
return new Promise<void>((resolve, reject) => {
const promises: Promise<void>[] = []
// LazyFieldComponent: () =>
// // @ts-ignore-next-line
// import('./field').then((module) => {
// const RichTextField = module.RichTextField
// return import('@payloadcms/ui').then((module2) =>
// module2.withMergedProps({
// Component: RichTextField,
// toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
// }),
// )
// }),
// afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
// return new Promise<void>((resolve, reject) => {
// const promises: Promise<void>[] = []
if (finalSanitizedEditorConfig?.features?.hooks?.afterReadPromises?.length) {
for (const afterReadPromise of finalSanitizedEditorConfig.features.hooks
.afterReadPromises) {
const promise = afterReadPromise({
field,
incomingEditorState,
siblingDoc,
})
if (promise) {
promises.push(promise)
}
}
}
// if (finalSanitizedEditorConfig?.features?.hooks?.afterReadPromises?.length) {
// for (const afterReadPromise of finalSanitizedEditorConfig.features.hooks
// .afterReadPromises) {
// const promise = afterReadPromise({
// field,
// incomingEditorState,
// siblingDoc,
// })
// if (promise) {
// promises.push(promise)
// }
// }
// }
Promise.all(promises)
.then(() => resolve())
.catch((error) => reject(error))
})
},
editorConfig: finalSanitizedEditorConfig,
generateComponentMap: getGenerateComponentMap({
resolvedFeatureMap: resolvedFeatureMap,
}),
outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
let outputSchema: JSONSchema4 = {
// This schema matches the SerializedEditorState type so far, that it's possible to cast SerializedEditorState to this schema without any errors.
// In the future, we should
// 1) allow recursive children
// 2) Pass in all the different types for every node added to the editorconfig. This can be done with refs in the schema.
type: withNullableJSONSchemaType('object', isRequired),
properties: {
root: {
type: 'object',
additionalProperties: false,
properties: {
type: {
type: 'string',
},
children: {
type: 'array',
items: {
type: 'object',
additionalProperties: true,
properties: {
type: {
type: 'string',
},
version: {
type: 'integer',
},
},
required: ['type', 'version'],
},
},
direction: {
oneOf: [
{
enum: ['ltr', 'rtl'],
},
{
type: 'null',
},
],
},
format: {
type: 'string',
enum: ['left', 'start', 'center', 'right', 'end', 'justify', ''], // ElementFormatType, since the root node is an element
},
indent: {
type: 'integer',
},
version: {
type: 'integer',
},
},
required: ['children', 'direction', 'format', 'indent', 'type', 'version'],
},
},
required: ['root'],
}
for (const modifyOutputSchema of finalSanitizedEditorConfig.features.generatedTypes
.modifyOutputSchemas) {
outputSchema = modifyOutputSchema({
currentSchema: outputSchema,
field,
interfaceNameDefinitions,
isRequired,
})
}
// Promise.all(promises)
// .then(() => resolve())
// .catch((error) => reject(error))
// })
// },
// editorConfig: finalSanitizedEditorConfig,
// outputSchema: ({ field, interfaceNameDefinitions, isRequired }) => {
// let outputSchema: JSONSchema4 = {
// // This schema matches the SerializedEditorState type so far, that it's possible to cast SerializedEditorState to this schema without any errors.
// // In the future, we should
// // 1) allow recursive children
// // 2) Pass in all the different types for every node added to the editorconfig. This can be done with refs in the schema.
// properties: {
// root: {
// additionalProperties: false,
// properties: {
// children: {
// items: {
// additionalProperties: true,
// properties: {
// type: {
// type: 'string',
// },
// version: {
// type: 'integer',
// },
// },
// required: ['type', 'version'],
// type: 'object',
// },
// type: 'array',
// },
// direction: {
// oneOf: [
// {
// enum: ['ltr', 'rtl'],
// },
// {
// type: 'null',
// },
// ],
// },
// format: {
// enum: ['left', 'start', 'center', 'right', 'end', 'justify', ''], // ElementFormatType, since the root node is an element
// type: 'string',
// },
// indent: {
// type: 'integer',
// },
// type: {
// type: 'string',
// },
// version: {
// type: 'integer',
// },
// },
// required: ['children', 'direction', 'format', 'indent', 'type', 'version'],
// type: 'object',
// },
// },
// required: ['root'],
// type: withNullableJSONSchemaType('object', isRequired),
// }
// for (const modifyOutputSchema of finalSanitizedEditorConfig.features.generatedTypes
// .modifyOutputSchemas) {
// outputSchema = modifyOutputSchema({
// currentSchema: outputSchema,
// field,
// interfaceNameDefinitions,
// isRequired,
// })
// }
//
// return outputSchema
// },
// populationPromise({
// context,
// currentDepth,
// depth,
// field,
// findMany,
// flattenLocales,
// overrideAccess,
// populationPromises,
// req,
// showHiddenFields,
// siblingDoc,
// }) {
// // check if there are any features with nodes which have populationPromises for this field
// if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
// return richTextRelationshipPromise({
// context,
// currentDepth,
// depth,
// editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
// field,
// findMany,
// flattenLocales,
// overrideAccess,
// populationPromises,
// req,
// showHiddenFields,
// siblingDoc,
// })
// }
return outputSchema
},
populationPromise({
context,
currentDepth,
depth,
field,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
}) {
// check if there are any features with nodes which have populationPromises for this field
if (finalSanitizedEditorConfig?.features?.populationPromises?.size) {
return richTextRelationshipPromise({
context,
currentDepth,
depth,
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
field,
findMany,
flattenLocales,
overrideAccess,
populationPromises,
req,
showHiddenFields,
siblingDoc,
})
}
// return null
// },
// validate: richTextValidateHOC({
// editorConfig: finalSanitizedEditorConfig,
// }),
// }
return null
},
validate: richTextValidateHOC({
editorConfig: finalSanitizedEditorConfig,
}),
}
}
export { BlockQuoteFeature } from './field/features/BlockQuote'
@@ -249,10 +262,10 @@ export {
} from './field/features/Relationship/nodes/RelationshipNode'
export { UploadFeature } from './field/features/Upload'
export type { UploadFeatureProps } from './field/features/Upload'
export type { RawUploadPayload } from './field/features/Upload/nodes/UploadNode'
export {
$createUploadNode,
$isUploadNode,
type RawUploadPayload,
type SerializedUploadNode,
type UploadData,
UploadNode,

View File

@@ -0,0 +1,13 @@
import { useAddClientFunction } from '@payloadcms/ui/providers'
import type { FeatureProvider } from './field/features/types'
import { useFieldPath } from '../../ui/src/forms/FieldPathProvider'
type FeatureProviderGetter = () => FeatureProvider
export const useLexicalFeature = (key: string, feature: FeatureProviderGetter) => {
const { schemaPath } = useFieldPath()
useAddClientFunction(`lexicalFeature.${schemaPath}.${key}`, feature)
}

View File

@@ -8,24 +8,44 @@ export type ITableCellContext = {
cellProps?: Partial<CellProps>
columnIndex?: number
customCellContext: CellComponentProps['customCellContext']
richTextComponentMap?: CellComponentProps['richTextComponentMap']
rowData: any
}
const TableCellContext = React.createContext<ITableCellContext>({} as ITableCellContext)
export const TableCellProvider: React.FC<{
cellData: any
cellData?: any
cellProps?: Partial<CellProps>
children: React.ReactNode
columnIndex?: number
customCellContext: CellComponentProps['customCellContext']
rowData: any
customCellContext?: CellComponentProps['customCellContext']
richTextComponentMap?: CellComponentProps['richTextComponentMap']
rowData?: any
}> = (props) => {
const { cellData, cellProps, children, columnIndex, customCellContext, rowData } = props
const {
cellData,
cellProps,
children,
columnIndex,
customCellContext,
richTextComponentMap,
rowData,
} = props
const contextToInherit = useTableCell()
return (
<TableCellContext.Provider
value={{ cellData, cellProps, columnIndex, customCellContext, rowData }}
value={{
cellData,
cellProps,
columnIndex,
customCellContext,
richTextComponentMap,
rowData,
...contextToInherit,
}}
>
{children}
</TableCellContext.Provider>

View File

@@ -32,7 +32,7 @@ export { useStepNav } from '../elements/StepNav'
export { SetStepNav } from '../elements/StepNav/SetStepNav'
export type { StepNavItem } from '../elements/StepNav/types'
export { Table } from '../elements/Table'
export { useTableCell } from '../elements/Table/TableCellProvider'
export { TableCellProvider, useTableCell } from '../elements/Table/TableCellProvider'
export type { Column } from '../elements/Table/types'
export { TableColumnsProvider } from '../elements/TableColumns'
export { default as Thumbnail } from '../elements/Thumbnail'

View File

@@ -2,6 +2,7 @@ export { mapFields } from '../utilities/buildComponentMap/mapFields'
export type { FieldMap, MappedField } from '../utilities/buildComponentMap/types'
export { buildFieldSchemaMap } from '../utilities/buildFieldSchemaMap'
export type { FieldSchemaMap } from '../utilities/buildFieldSchemaMap/types'
export { default as canUseDOM } from '../utilities/canUseDOM'
export { findLocaleFromCode } from '../utilities/findLocaleFromCode'
export { formatDate } from '../utilities/formatDate'
export { formatDocTitle } from '../utilities/formatDocTitle'

View File

@@ -14,8 +14,6 @@ import type {
} from 'payload/types'
import type { Option } from 'payload/types'
import { RichTextField } from 'payload/types'
import type { FormState } from '../..'
import type {
FieldMap,

View File

@@ -297,6 +297,7 @@ export const mapFields = (args: {
const result = field.editor.generateComponentMap({ config, schemaPath: path })
// @ts-expect-error-next-line // TODO: the `richTextComponentMap` is not found on the union type
fieldComponentProps.richTextComponentMap = result
cellComponentProps.richTextComponentMap = result
}
if (RichTextFieldComponent) {
@@ -304,7 +305,7 @@ export const mapFields = (args: {
}
if (RichTextCellComponent) {
cellComponentProps.CellComponentOverride = RichTextCellComponent
cellComponentProps.CellComponentOverride = <RichTextCellComponent />
}
}