diff --git a/packages/next/src/pages/List/Default/Cell/fields/Relationship/index.tsx b/packages/next/src/pages/List/Default/Cell/fields/Relationship/index.tsx index 7c94269e6e..f3b324f525 100644 --- a/packages/next/src/pages/List/Default/Cell/fields/Relationship/index.tsx +++ b/packages/next/src/pages/List/Default/Cell/fields/Relationship/index.tsx @@ -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 = ({ 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) { diff --git a/packages/next/src/pages/List/Default/Cell/index.tsx b/packages/next/src/pages/List/Default/Cell/index.tsx index 89522fbb5b..6dba3fddf5 100644 --- a/packages/next/src/pages/List/Default/Cell/index.tsx +++ b/packages/next/src/pages/List/Default/Cell/index.tsx @@ -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 = (props) => { isFieldAffectingData, label, onClick: onClickFromProps, + richTextComponentMap, } = props const { i18n } = useTranslation() @@ -77,12 +79,35 @@ export const DefaultCell: React.FC = (props) => { ) } - let CellComponent: React.FC = - cellData && (CellComponentOverride ? CellComponentOverride : cellComponents[fieldType]) + const DefaultCellComponent = cellComponents[fieldType] - if (!CellComponent) { + let CellComponent: React.ReactNode = + cellData && + (CellComponentOverride ? ( // CellComponentOverride is used for richText + + {CellComponentOverride} + + ) : null) + + if (!CellComponent && DefaultCellComponent) { + CellComponent = ( + + ) + } 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 = ( + + ) } else { return ( @@ -99,9 +124,5 @@ export const DefaultCell: React.FC = (props) => { } } - return ( - - - - ) + return {CellComponent} } diff --git a/packages/payload/src/admin/RichText.ts b/packages/payload/src/admin/RichText.ts index 03b4e469b4..ce520ca38e 100644 --- a/packages/payload/src/admin/RichText.ts +++ b/packages/payload/src/admin/RichText.ts @@ -31,7 +31,7 @@ type RichTextAdapterBase< config: SanitizedConfig schemaPath: string }) => Map - generateSchemaMap: (args: { + generateSchemaMap?: (args: { config: SanitizedConfig schemaMap: Map schemaPath: string diff --git a/packages/payload/src/admin/elements/Cell.ts b/packages/payload/src/admin/elements/Cell.ts index 9b95478b0c..f3cb8f097d 100644 --- a/packages/payload/src/admin/elements/Cell.ts +++ b/packages/payload/src/admin/elements/Cell.ts @@ -16,7 +16,7 @@ export type CellProps = { * * This is used to provide the RichText cell component for the RichText field. */ - CellComponentOverride?: React.ComponentType + 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 // any should be MappedField } export type CellComponentProps = { @@ -44,5 +45,6 @@ export type CellComponentProps = { collectionSlug?: SanitizedCollectionConfig['slug'] uploadConfig?: SanitizedCollectionConfig['upload'] } + richTextComponentMap?: Map rowData?: Record } diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index 3840262962..0d2f43477e 100644 --- a/packages/richtext-lexical/package.json +++ b/packages/richtext-lexical/package.json @@ -49,7 +49,9 @@ "payload": "workspace:*" }, "peerDependencies": { - "payload": "^2.4.0" + "payload": "^2.4.0", + "@payloadcms/translations": "workspace:^", + "@payloadcms/ui": "workspace:^" }, "exports": { ".": { diff --git a/packages/richtext-lexical/src/cell/index.tsx b/packages/richtext-lexical/src/cell/index.tsx index ddea6d5b26..409070cb5e 100644 --- a/packages/richtext-lexical/src/cell/index.tsx +++ b/packages/richtext-lexical/src/cell/index.tsx @@ -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 -> = ({ 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 {preview} } diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 67dd9db73d..8c80c71f63 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -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 = (props) => { +const RichText: React.FC< + FormFieldBase & { + editorConfig: SanitizedEditorConfig // With rendered features n stuff + name: string + richTextComponentMap: Map + } +> = (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 = (props) => { ) const fieldType = useField({ - // 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 = (props) => { }} >
- -
) } -function fallbackRender({ error }): JSX.Element { +function fallbackRender({ error }): React.ReactElement { // Call resetErrorBoundary() to reset the error boundary and retry the render. return ( diff --git a/packages/richtext-lexical/src/field/features/Paragraph/Component.tsx b/packages/richtext-lexical/src/field/features/Paragraph/Component.tsx new file mode 100644 index 0000000000..587fd37b8c --- /dev/null +++ b/packages/richtext-lexical/src/field/features/Paragraph/Component.tsx @@ -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 +} diff --git a/packages/richtext-lexical/src/field/features/Paragraph/index.ts b/packages/richtext-lexical/src/field/features/Paragraph/index.ts index 0cae5ae91e..e0bd9a2df1 100644 --- a/packages/richtext-lexical/src/field/features/Paragraph/index.ts +++ b/packages/richtext-lexical/src/field/features/Paragraph/index.ts @@ -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, } } diff --git a/packages/richtext-lexical/src/field/features/Upload/nodes/UploadNode.tsx b/packages/richtext-lexical/src/field/features/Upload/nodes/UploadNode.tsx index c486ea266c..dec5fde782 100644 --- a/packages/richtext-lexical/src/field/features/Upload/nodes/UploadNode.tsx +++ b/packages/richtext-lexical/src/field/features/Upload/nodes/UploadNode.tsx @@ -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 } diff --git a/packages/richtext-lexical/src/field/features/migrations/LexicalPluginToLexical/index.ts b/packages/richtext-lexical/src/field/features/migrations/LexicalPluginToLexical/index.ts index e65062a7ec..7dd855a7b2 100644 --- a/packages/richtext-lexical/src/field/features/migrations/LexicalPluginToLexical/index.ts +++ b/packages/richtext-lexical/src/field/features/migrations/LexicalPluginToLexical/index.ts @@ -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 }) { diff --git a/packages/richtext-lexical/src/field/features/types.ts b/packages/richtext-lexical/src/field/features/types.ts index 072c6225a1..3709240a1b 100644 --- a/packages/richtext-lexical/src/field/features/types.ts +++ b/packages/richtext-lexical/src/field/features/types.ts @@ -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 + Pick< + FeatureProvider, + 'Component' | 'dependencies' | 'dependenciesPriority' | 'dependenciesSoft' | 'key' + > > export type ResolvedFeatureMap = Map diff --git a/packages/richtext-lexical/src/field/index.tsx b/packages/richtext-lexical/src/field/index.tsx index 76d7fcf7c2..bae8799fe3 100644 --- a/packages/richtext-lexical/src/field/index.tsx +++ b/packages/richtext-lexical/src/field/index.tsx @@ -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 = (props) => { +export const RichTextField: React.FC< + FormFieldBase & { + lexicalEditorConfig: LexicalEditorConfig + name: string + richTextComponentMap: Map + } +> = (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([]) + + 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 ( + + {Array.isArray(featureProviders) && + featureProviders.map((FeatureProvider, i) => { + return {FeatureProvider} + })} + + ) + } + + const features = clientFunctions + + console.log('clientFunctions', features['lexicalFeature.posts.richText.paragraph']) + return ( }> - + ) } diff --git a/packages/richtext-lexical/src/field/lexical/config/loader.ts b/packages/richtext-lexical/src/field/lexical/config/loader.ts index 2b921f1438..7ea85591fc 100644 --- a/packages/richtext-lexical/src/field/lexical/config/loader.ts +++ b/packages/richtext-lexical/src/field/lexical/config/loader.ts @@ -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, diff --git a/packages/richtext-lexical/src/field/lexical/config/sanitize.ts b/packages/richtext-lexical/src/field/lexical/config/sanitize.ts index bbb8e66ada..2aa1d8ab17 100644 --- a/packages/richtext-lexical/src/field/lexical/config/sanitize.ts +++ b/packages/richtext-lexical/src/field/lexical/config/sanitize.ts @@ -171,7 +171,7 @@ export const sanitizeFeatures = (features: ResolvedFeatureMap): SanitizedFeature export function sanitizeEditorConfig(editorConfig: EditorConfig): SanitizedEditorConfig { const resolvedFeatureMap = loadFeatures({ - unsanitizedEditorConfig: editorConfig, + unSanitizedEditorConfig: editorConfig, }) return { diff --git a/packages/richtext-lexical/src/generateComponentMap.tsx b/packages/richtext-lexical/src/generateComponentMap.tsx new file mode 100644 index 0000000000..a6654435c9 --- /dev/null +++ b/packages/richtext-lexical/src/generateComponentMap.tsx @@ -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}`, ) + } + + console.log('componentMaaap', componentMap) + return componentMap + } diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 152ea529a1..53e7047c67 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -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 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((resolve, reject) => { + const promises: Promise[] = [] - // 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((resolve, reject) => { - // const promises: 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) + } + } + } - // 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, diff --git a/packages/richtext-lexical/src/useLexicalFeature.tsx b/packages/richtext-lexical/src/useLexicalFeature.tsx new file mode 100644 index 0000000000..da8de43804 --- /dev/null +++ b/packages/richtext-lexical/src/useLexicalFeature.tsx @@ -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) +} diff --git a/packages/ui/src/elements/Table/TableCellProvider/index.tsx b/packages/ui/src/elements/Table/TableCellProvider/index.tsx index 7d99a4105b..70ad731092 100644 --- a/packages/ui/src/elements/Table/TableCellProvider/index.tsx +++ b/packages/ui/src/elements/Table/TableCellProvider/index.tsx @@ -8,24 +8,44 @@ export type ITableCellContext = { cellProps?: Partial columnIndex?: number customCellContext: CellComponentProps['customCellContext'] + richTextComponentMap?: CellComponentProps['richTextComponentMap'] rowData: any } const TableCellContext = React.createContext({} as ITableCellContext) export const TableCellProvider: React.FC<{ - cellData: any + cellData?: any cellProps?: Partial 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 ( {children} diff --git a/packages/ui/src/exports/elements.ts b/packages/ui/src/exports/elements.ts index 517f7950bc..8b9dfa2092 100644 --- a/packages/ui/src/exports/elements.ts +++ b/packages/ui/src/exports/elements.ts @@ -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' diff --git a/packages/ui/src/exports/utilities.ts b/packages/ui/src/exports/utilities.ts index b1101a6d5a..e3b2108412 100644 --- a/packages/ui/src/exports/utilities.ts +++ b/packages/ui/src/exports/utilities.ts @@ -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' diff --git a/packages/ui/src/forms/fields/shared.ts b/packages/ui/src/forms/fields/shared.ts index e2d96261c1..cb706a23dd 100644 --- a/packages/ui/src/forms/fields/shared.ts +++ b/packages/ui/src/forms/fields/shared.ts @@ -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, diff --git a/packages/ui/src/utilities/buildComponentMap/mapFields.tsx b/packages/ui/src/utilities/buildComponentMap/mapFields.tsx index f2225a3198..24d5243245 100644 --- a/packages/ui/src/utilities/buildComponentMap/mapFields.tsx +++ b/packages/ui/src/utilities/buildComponentMap/mapFields.tsx @@ -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 = } }