Merge branch 'feat/next-poc' of https://github.com/payloadcms/payload into feat/next-poc
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"payload": "^2.4.0"
|
||||
"payload": "^2.4.0",
|
||||
"@payloadcms/translations": "workspace:^",
|
||||
"@payloadcms/ui": "workspace:^"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -171,7 +171,7 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature
|
||||
|
||||
export function sanitizeEditorConfig(editorConfig: EditorConfig): SanitizedEditorConfig {
|
||||
const resolvedFeatureMap = loadFeatures({
|
||||
unsanitizedEditorConfig: editorConfig,
|
||||
unSanitizedEditorConfig: editorConfig,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
23
packages/richtext-lexical/src/generateComponentMap.tsx
Normal file
23
packages/richtext-lexical/src/generateComponentMap.tsx
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
13
packages/richtext-lexical/src/useLexicalFeature.tsx
Normal file
13
packages/richtext-lexical/src/useLexicalFeature.tsx
Normal 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)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user