perf(richtext-lexical)!: greatly simplify lexical loading and improve performance (#8041)

We noticed that we can bring functions down to the client directly
without having to wrap them in a component first. This greatly
simplifies the loading of all lexical client components

**BREAKING:**
- `createClientComponent` is no longer exported as it's not needed
anymore
- The exported `ClientComponentProps` type has been renamed to
`BaseClientFeatureProps`.
- The order of arguments in `sanitizeClientEditorConfig` has changed
This commit is contained in:
Alessio Gravili
2024-09-03 12:48:41 -04:00
committed by GitHub
parent 11576eda13
commit b6a8d1c461
17 changed files with 138 additions and 378 deletions

View File

@@ -3,9 +3,9 @@ import type { EditorConfig as LexicalEditorConfig } from 'lexical'
import type { CellComponentProps, RichTextFieldClient } from 'payload' import type { CellComponentProps, RichTextFieldClient } from 'payload'
import { createHeadlessEditor } from '@lexical/headless' import { createHeadlessEditor } from '@lexical/headless'
import { useClientFunctions, useTableCell } from '@payloadcms/ui' import { useTableCell } from '@payloadcms/ui'
import { $getRoot } from 'lexical' import { $getRoot } from 'lexical'
import React, { useEffect, useState } from 'react' import React, { useEffect, useMemo } from 'react'
import type { FeatureProviderClient } from '../features/typesClient.js' import type { FeatureProviderClient } from '../features/typesClient.js'
import type { SanitizedClientEditorConfig } from '../lexical/config/types.js' import type { SanitizedClientEditorConfig } from '../lexical/config/types.js'
@@ -24,7 +24,7 @@ export const RichTextCell: React.FC<
> = (props) => { > = (props) => {
const { const {
admin, admin,
field: { _schemaPath, richTextComponentMap }, field: { richTextComponentMap },
lexicalEditorConfig, lexicalEditorConfig,
} = props } = props
@@ -32,90 +32,35 @@ export const RichTextCell: React.FC<
const { cellData } = useTableCell() const { cellData } = useTableCell()
const clientFunctions = useClientFunctions() const finalSanitizedEditorConfig = useMemo<SanitizedClientEditorConfig>(() => {
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false) const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
'features',
) as GeneratedFeatureProviderComponent[]
const [featureProviders, setFeatureProviders] = useState< const featureProvidersLocal: FeatureProviderClient<any, any>[] = []
FeatureProviderClient<unknown, unknown>[] for (const clientFeature of clientFeatures) {
>([]) featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps))
}
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] = const finalLexicalEditorConfig = lexicalEditorConfig
useState<SanitizedClientEditorConfig>(null) ? lexicalEditorConfig
: defaultEditorLexicalConfig
const featureProviderComponents: GeneratedFeatureProviderComponent[] = ( const resolvedClientFeatures = loadClientFeatures({
richTextComponentMap.get('features') as GeneratedFeatureProviderComponent[] unSanitizedEditorConfig: {
).sort((a, b) => a.order - b.order) // order by order features: featureProvidersLocal,
lexical: finalLexicalEditorConfig,
},
})
let featureProvidersAndComponentsToLoad = 0 // feature providers and components return sanitizeClientEditorConfig(resolvedClientFeatures, finalLexicalEditorConfig)
for (const featureProvider of featureProviderComponents) { }, [richTextComponentMap, lexicalEditorConfig])
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
key.startsWith(`feature.${featureProvider.key}.components.`),
)
featureProvidersAndComponentsToLoad += 1 finalSanitizedEditorConfig.admin = admin
featureProvidersAndComponentsToLoad += featureComponentKeys.length
}
useEffect(() => { useEffect(() => {
if (!hasLoadedFeatures) {
const featureProvidersLocal: FeatureProviderClient<unknown, unknown>[] = []
let featureProvidersAndComponentsLoaded = 0 // feature providers and components only
Object.entries(clientFunctions).forEach(([key, plugin]) => {
if (key.startsWith(`lexicalFeature.${_schemaPath}.`)) {
if (!key.includes('.lexical_internal_components.')) {
featureProvidersLocal.push(plugin)
}
featureProvidersAndComponentsLoaded++
}
})
if (featureProvidersAndComponentsLoaded === featureProvidersAndComponentsToLoad) {
setFeatureProviders(featureProvidersLocal)
setHasLoadedFeatures(true)
/**
* Loaded feature provided => create the final sanitized editor config
*/
const resolvedClientFeatures = loadClientFeatures({
clientFunctions,
schemaPath: _schemaPath,
unSanitizedEditorConfig: {
features: featureProvidersLocal,
lexical: lexicalEditorConfig,
},
})
setFinalSanitizedEditorConfig(
sanitizeClientEditorConfig(
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
resolvedClientFeatures,
admin,
),
)
}
}
}, [
admin,
featureProviderComponents,
hasLoadedFeatures,
clientFunctions,
_schemaPath,
featureProviderComponents.length,
featureProviders,
finalSanitizedEditorConfig,
lexicalEditorConfig,
richTextComponentMap,
featureProvidersAndComponentsToLoad,
])
useEffect(() => {
if (!hasLoadedFeatures) {
return
}
let dataToUse = cellData let dataToUse = cellData
if (dataToUse == null || !hasLoadedFeatures || !finalSanitizedEditorConfig) { if (dataToUse == null || !finalSanitizedEditorConfig) {
setPreview('') setPreview('')
return return
} }
@@ -159,38 +104,7 @@ export const RichTextCell: React.FC<
// Limiting the number of characters shown is done in a CSS rule // Limiting the number of characters shown is done in a CSS rule
setPreview(textContent) setPreview(textContent)
}, [cellData, lexicalEditorConfig, hasLoadedFeatures, finalSanitizedEditorConfig]) }, [cellData, finalSanitizedEditorConfig])
if (!hasLoadedFeatures) {
return (
<React.Fragment>
{Array.isArray(featureProviderComponents) &&
featureProviderComponents.map((featureProvider) => {
// get all components starting with key feature.${FeatureProvider.key}.components.{featureComponentKey}
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
key.startsWith(
`lexical_internal_feature.${featureProvider.key}.lexical_internal_components.`,
),
)
const featureComponents: React.ReactNode[] = featureComponentKeys.map((key) => {
return richTextComponentMap.get(key)
}) as React.ReactNode[]
return (
<React.Fragment key={featureProvider.key}>
{featureComponents?.length
? featureComponents.map((FeatureComponent) => {
return FeatureComponent
})
: null}
{featureProvider.ClientFeature}
</React.Fragment>
)
})}
</React.Fragment>
)
}
return <span>{preview}</span> return <span>{preview}</span>
} }

View File

@@ -7,7 +7,6 @@ export { RichTextCell } from '../../cell/index.js'
export { AlignFeatureClient } from '../../features/align/client/index.js' export { AlignFeatureClient } from '../../features/align/client/index.js'
export { BlockquoteFeatureClient } from '../../features/blockquote/client/index.js' export { BlockquoteFeatureClient } from '../../features/blockquote/client/index.js'
export { BlocksFeatureClient } from '../../features/blocks/client/index.js' export { BlocksFeatureClient } from '../../features/blocks/client/index.js'
export { createClientComponent } from '../../features/createClientComponent.js'
export { TestRecorderFeatureClient } from '../../features/debug/testRecorder/client/index.js' export { TestRecorderFeatureClient } from '../../features/debug/testRecorder/client/index.js'
export { TreeViewFeatureClient } from '../../features/debug/treeView/client/index.js' export { TreeViewFeatureClient } from '../../features/debug/treeView/client/index.js'
export { BoldFeatureClient } from '../../features/format/bold/feature.client.js' export { BoldFeatureClient } from '../../features/format/bold/feature.client.js'

View File

@@ -1,19 +0,0 @@
'use client'
import type React from 'react'
import type { ClientComponentProps, FeatureProviderProviderClient } from './typesClient.js'
import { useLexicalFeature } from '../utilities/useLexicalFeature.js'
/**
* Utility function to create a client component for the client feature
*/
export const createClientComponent = <ClientFeatureProps,>(
clientFeature: FeatureProviderProviderClient<ClientFeatureProps>,
): React.FC<ClientComponentProps<ClientFeatureProps>> => {
return (props) => {
useLexicalFeature(props.featureKey, clientFeature(props))
return null
}
}

View File

@@ -1,28 +0,0 @@
'use client'
import type React from 'react'
import { useAddClientFunction, useFieldProps, useTableCell } from '@payloadcms/ui'
const useLexicalFeatureProp = <T,>(featureKey: string, componentKey: string, prop: T) => {
const { schemaPath: schemaPathFromFieldProps } = useFieldProps()
const tableCell = useTableCell()
const schemaPathFromCellProps = tableCell?.cellProps?.field?._schemaPath
const schemaPath = schemaPathFromCellProps || schemaPathFromFieldProps // schemaPathFromCellProps needs to have priority, as there can be cells within fields (e.g. list drawers) and the cell schemaPath needs to be used there - not the parent field schemaPath. There cannot be fields within cells.
useAddClientFunction(
`lexicalFeature.${schemaPath}.${featureKey}.lexical_internal_components.${componentKey}`,
prop,
)
}
export const createFeaturePropComponent = <T = unknown,>(
prop: T,
): React.FC<{ componentKey: string; featureKey: string }> => {
return (props) => {
useLexicalFeatureProp(props.featureKey, props.componentKey, prop)
return null
}
}

View File

@@ -3,7 +3,7 @@
import { createClientFeature } from '../../../utilities/createClientFeature.js' import { createClientFeature } from '../../../utilities/createClientFeature.js'
import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js' import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js'
export const SlateToLexicalFeatureClient = createClientFeature(({ clientFunctions }) => { export const SlateToLexicalFeatureClient = createClientFeature(() => {
return { return {
nodes: [UnknownConvertedNode], nodes: [UnknownConvertedNode],
} }

View File

@@ -15,7 +15,7 @@ import type { ToolbarGroup } from './toolbars/types.js'
export type FeatureProviderProviderClient< export type FeatureProviderProviderClient<
UnSanitizedClientFeatureProps = undefined, UnSanitizedClientFeatureProps = undefined,
ClientFeatureProps = UnSanitizedClientFeatureProps, ClientFeatureProps = UnSanitizedClientFeatureProps,
> = (props: ClientComponentProps<ClientFeatureProps>) => FeatureProviderClient<ClientFeatureProps> > = (props: BaseClientFeatureProps<ClientFeatureProps>) => FeatureProviderClient<ClientFeatureProps>
/** /**
* No dependencies => Features need to be sorted on the server first, then sent to client in right order * No dependencies => Features need to be sorted on the server first, then sent to client in right order
@@ -27,10 +27,9 @@ export type FeatureProviderClient<
/** /**
* Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to
*/ */
clientFeatureProps: ClientComponentProps<UnSanitizedClientFeatureProps> clientFeatureProps: BaseClientFeatureProps<UnSanitizedClientFeatureProps>
feature: feature:
| ((props: { | ((props: {
clientFunctions: Record<string, any>
/** unSanitizedEditorConfig.features, but mapped */ /** unSanitizedEditorConfig.features, but mapped */
featureProviderMap: ClientFeatureProviderMap featureProviderMap: ClientFeatureProviderMap
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
@@ -105,7 +104,7 @@ export type ClientFeature<ClientFeatureProps> = {
/** /**
* Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to
*/ */
sanitizedClientFeatureProps?: ClientComponentProps<ClientFeatureProps> sanitizedClientFeatureProps?: BaseClientFeatureProps<ClientFeatureProps>
slashMenu?: { slashMenu?: {
/** /**
* Dynamic groups allow you to add different groups depending on the query string (so, the text after the slash). * Dynamic groups allow you to add different groups depending on the query string (so, the text after the slash).
@@ -143,7 +142,7 @@ export type ClientFeature<ClientFeatureProps> = {
} }
} }
export type ClientComponentProps<ClientFeatureProps> = ClientFeatureProps extends undefined export type BaseClientFeatureProps<ClientFeatureProps> = ClientFeatureProps extends undefined
? { ? {
featureKey: string featureKey: string
order: number order: number

View File

@@ -26,7 +26,7 @@ import type {
import type { ServerEditorConfig } from '../lexical/config/types.js' import type { ServerEditorConfig } from '../lexical/config/types.js'
import type { AdapterProps } from '../types.js' import type { AdapterProps } from '../types.js'
import type { HTMLConverter } from './converters/html/converter/types.js' import type { HTMLConverter } from './converters/html/converter/types.js'
import type { ClientComponentProps } from './typesClient.js' import type { BaseClientFeatureProps } from './typesClient.js'
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
context, context,
@@ -282,7 +282,7 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
} }
export type ServerFeature<ServerProps, ClientFeatureProps> = { export type ServerFeature<ServerProps, ClientFeatureProps> = {
ClientFeature?: PayloadComponent<never, ClientComponentProps<ClientFeatureProps>> ClientFeature?: PayloadComponent<never, BaseClientFeatureProps<ClientFeatureProps>>
/** /**
* This determines what props will be available on the Client. * This determines what props will be available on the Client.
*/ */

View File

@@ -28,7 +28,7 @@ import {
} from 'lexical' } from 'lexical'
import React, { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react' import React, { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react'
import type { ClientComponentProps } from '../../../typesClient.js' import type { BaseClientFeatureProps } from '../../../typesClient.js'
import type { UploadData } from '../../server/nodes/UploadNode.js' import type { UploadData } from '../../server/nodes/UploadNode.js'
import type { UploadFeaturePropsClient } from '../feature.client.js' import type { UploadFeaturePropsClient } from '../feature.client.js'
import type { UploadNode } from '../nodes/UploadNode.js' import type { UploadNode } from '../nodes/UploadNode.js'
@@ -138,38 +138,41 @@ const Component: React.FC<ElementProps> = (props) => {
}, },
[isSelected, nodeKey], [isSelected, nodeKey],
) )
const onClick = useCallback(
(event: MouseEvent) => {
// Check if uploadRef.target or anything WITHIN uploadRef.target was clicked
if (event.target === uploadRef.current || uploadRef.current?.contains(event.target as Node)) {
if (event.shiftKey) {
setSelected(!isSelected)
} else {
if (!isSelected) {
clearSelection()
setSelected(true)
}
}
return true
}
return false
},
[isSelected, setSelected, clearSelection],
)
useEffect(() => { useEffect(() => {
return mergeRegister( return mergeRegister(
editor.registerCommand<MouseEvent>(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW), editor.registerCommand<MouseEvent>(
CLICK_COMMAND,
(event: MouseEvent) => {
// Check if uploadRef.target or anything WITHIN uploadRef.target was clicked
if (
event.target === uploadRef.current ||
uploadRef.current?.contains(event.target as Node)
) {
if (event.shiftKey) {
setSelected(!isSelected)
} else {
if (!isSelected) {
clearSelection()
setSelected(true)
}
}
return true
}
return false
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(KEY_DELETE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_DELETE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
editor.registerCommand(KEY_BACKSPACE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_BACKSPACE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW),
) )
}, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected, onClick]) }, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected])
const hasExtraFields = ( const hasExtraFields = (
editorConfig?.resolvedFeatureMap?.get('upload') editorConfig?.resolvedFeatureMap?.get('upload')
?.sanitizedClientFeatureProps as ClientComponentProps<UploadFeaturePropsClient> ?.sanitizedClientFeatureProps as BaseClientFeatureProps<UploadFeaturePropsClient>
).collections?.[relatedCollection.slug]?.hasExtraFields ).collections?.[relatedCollection.slug]?.hasExtraFields
const onExtraFieldsDrawerSubmit = useCallback( const onExtraFieldsDrawerSubmit = useCallback(
@@ -272,7 +275,7 @@ const Component: React.FC<ElementProps> = (props) => {
</DocumentDrawerToggler> </DocumentDrawerToggler>
</div> </div>
</div> </div>
{value && <DocumentDrawer onSave={updateUpload} />} {value ? <DocumentDrawer onSave={updateUpload} /> : null}
{hasExtraFields ? ( {hasExtraFields ? (
<FieldsDrawer <FieldsDrawer
data={fields} data={fields}

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { ShimmerEffect, useClientFunctions, useFieldProps } from '@payloadcms/ui' import { ShimmerEffect } from '@payloadcms/ui'
import React, { lazy, Suspense, useEffect, useState } from 'react' import React, { lazy, Suspense, useEffect, useState } from 'react'
import type { FeatureProviderClient } from '../features/typesClient.js' import type { FeatureProviderClient } from '../features/typesClient.js'
@@ -21,117 +21,38 @@ export const RichTextField: React.FC<LexicalRichTextFieldProps> = (props) => {
field: { richTextComponentMap }, field: { richTextComponentMap },
lexicalEditorConfig, lexicalEditorConfig,
} = props } = props
const { schemaPath } = useFieldProps()
const clientFunctions = useClientFunctions()
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
const [featureProviders, setFeatureProviders] = useState<
FeatureProviderClient<unknown, unknown>[]
>([])
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] = const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
useState<SanitizedClientEditorConfig>(null) useState<SanitizedClientEditorConfig>(null)
let featureProviderComponents: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
'features',
) as GeneratedFeatureProviderComponent[]
// order by order
featureProviderComponents = featureProviderComponents.sort((a, b) => a.order - b.order)
let featureProvidersAndComponentsToLoad = 0 // feature providers and components
for (const featureProvider of featureProviderComponents) {
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
key.startsWith(
`lexical_internal_feature.${featureProvider.key}.lexical_internal_components.`,
),
)
featureProvidersAndComponentsToLoad += 1
featureProvidersAndComponentsToLoad += featureComponentKeys.length
}
useEffect(() => { useEffect(() => {
if (!hasLoadedFeatures) { if (finalSanitizedEditorConfig) {
const featureProvidersLocal: FeatureProviderClient<unknown, unknown>[] = [] return
let featureProvidersAndComponentsLoaded = 0
Object.entries(clientFunctions).forEach(([key, plugin]) => {
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
if (!key.includes('.lexical_internal_components.')) {
featureProvidersLocal.push(plugin)
}
featureProvidersAndComponentsLoaded++
}
})
if (featureProvidersAndComponentsLoaded === featureProvidersAndComponentsToLoad) {
setFeatureProviders(featureProvidersLocal)
setHasLoadedFeatures(true)
/**
* Loaded feature provided => create the final sanitized editor config
*/
const resolvedClientFeatures = loadClientFeatures({
clientFunctions,
schemaPath,
unSanitizedEditorConfig: {
features: featureProvidersLocal,
lexical: lexicalEditorConfig,
},
})
setFinalSanitizedEditorConfig(
sanitizeClientEditorConfig(
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
resolvedClientFeatures,
admin,
),
)
}
} }
}, [ const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
admin, 'features',
hasLoadedFeatures, ) as GeneratedFeatureProviderComponent[]
clientFunctions,
schemaPath,
featureProviderComponents.length,
featureProviders,
finalSanitizedEditorConfig,
lexicalEditorConfig,
featureProvidersAndComponentsToLoad,
])
if (!hasLoadedFeatures) { const featureProvidersLocal: FeatureProviderClient<any, any>[] = []
return ( for (const clientFeature of clientFeatures) {
<React.Fragment> featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps))
{Array.isArray(featureProviderComponents) && }
featureProviderComponents.map((featureProvider) => {
// get all components starting with key feature.${FeatureProvider.key}.components.{featureComponentKey}
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
key.startsWith(
`lexical_internal_feature.${featureProvider.key}.lexical_internal_components.`,
),
)
const featureComponents: React.ReactNode[] = featureComponentKeys.map((key) => {
return richTextComponentMap.get(key)
}) as React.ReactNode[] // TODO: Type better
return ( const finalLexicalEditorConfig = lexicalEditorConfig
<React.Fragment key={featureProvider.key}> ? lexicalEditorConfig
{featureComponents?.length : defaultEditorLexicalConfig
? featureComponents.map((FeatureComponent) => {
return FeatureComponent const resolvedClientFeatures = loadClientFeatures({
}) unSanitizedEditorConfig: {
: null} features: featureProvidersLocal,
{featureProvider.ClientFeature} lexical: finalLexicalEditorConfig,
</React.Fragment> },
) })
})}
</React.Fragment> setFinalSanitizedEditorConfig(
sanitizeClientEditorConfig(resolvedClientFeatures, finalLexicalEditorConfig, admin),
) )
} }, [lexicalEditorConfig, richTextComponentMap, admin, finalSanitizedEditorConfig]) // TODO: Optimize this and use useMemo for this in the future. This might break sub-richtext-blocks from the blocks feature. Need to investigate
return ( return (
<Suspense fallback={<ShimmerEffect height="35vh" />}> <Suspense fallback={<ShimmerEffect height="35vh" />}>

View File

@@ -889,7 +889,7 @@ export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js
export type { ToolbarGroup, ToolbarGroupItem } from './features/toolbars/types.js' export type { ToolbarGroup, ToolbarGroupItem } from './features/toolbars/types.js'
export type { export type {
ClientComponentProps, BaseClientFeatureProps,
ClientFeature, ClientFeature,
ClientFeatureProviderMap, ClientFeatureProviderMap,
FeatureProviderClient, FeatureProviderClient,

View File

@@ -2,7 +2,7 @@
import type { import type {
ClientFeatureProviderMap, ClientFeatureProviderMap,
FeatureProviderClient, ResolvedClientFeature,
ResolvedClientFeatureMap, ResolvedClientFeatureMap,
} from '../../../features/typesClient.js' } from '../../../features/typesClient.js'
import type { ClientEditorConfig } from '../types.js' import type { ClientEditorConfig } from '../types.js'
@@ -12,12 +12,8 @@ import type { ClientEditorConfig } from '../types.js'
* @param unSanitizedEditorConfig * @param unSanitizedEditorConfig
*/ */
export function loadClientFeatures({ export function loadClientFeatures({
clientFunctions,
schemaPath,
unSanitizedEditorConfig, unSanitizedEditorConfig,
}: { }: {
clientFunctions?: Record<string, any>
schemaPath: string
unSanitizedEditorConfig: ClientEditorConfig unSanitizedEditorConfig: ClientEditorConfig
}): ResolvedClientFeatureMap { }): ResolvedClientFeatureMap {
for (const featureProvider of unSanitizedEditorConfig.features) { for (const featureProvider of unSanitizedEditorConfig.features) {
@@ -37,50 +33,32 @@ export function loadClientFeatures({
(a, b) => a.clientFeatureProps.order - b.clientFeatureProps.order, (a, b) => a.clientFeatureProps.order - b.clientFeatureProps.order,
) )
const featureProviderMap: ClientFeatureProviderMap = new Map( const featureProviderMap: ClientFeatureProviderMap = new Map()
unSanitizedEditorConfig.features.map( for (const feature of unSanitizedEditorConfig.features) {
(f) => featureProviderMap.set(feature.clientFeatureProps.featureKey, feature)
[f.clientFeatureProps.featureKey, f] as [string, FeatureProviderClient<unknown, unknown>], }
),
)
const resolvedFeatures: ResolvedClientFeatureMap = new Map() const resolvedFeatures: ResolvedClientFeatureMap = new Map()
// Make sure all dependencies declared in the respective features exist // Make sure all dependencies declared in the respective features exist
let loaded = 0 let loaded = 0
for (const featureProvider of unSanitizedEditorConfig.features) { for (const featureProvider of unSanitizedEditorConfig.features) {
/** const feature: Partial<ResolvedClientFeature<any>> =
* Load relevant clientFunctions scoped to this feature and then pass them to the client feature
*/
const relevantClientFunctions: Record<string, any> = {}
Object.entries(clientFunctions).forEach(([key, plugin]) => {
if (
key.startsWith(
`lexicalFeature.${schemaPath}.${featureProvider.clientFeatureProps.featureKey}.components.`,
)
) {
const featureComponentKey = key.split(
`${schemaPath}.${featureProvider.clientFeatureProps.featureKey}.components.`,
)[1]
relevantClientFunctions[featureComponentKey] = plugin
}
})
const feature =
typeof featureProvider.feature === 'function' typeof featureProvider.feature === 'function'
? featureProvider.feature({ ? featureProvider.feature({
clientFunctions: relevantClientFunctions,
featureProviderMap, featureProviderMap,
resolvedFeatures, resolvedFeatures,
unSanitizedEditorConfig, unSanitizedEditorConfig,
}) })
: featureProvider.feature : featureProvider.feature
resolvedFeatures.set(featureProvider.clientFeatureProps.featureKey, { feature.key = featureProvider.clientFeatureProps.featureKey
...feature, feature.order = loaded
key: featureProvider.clientFeatureProps.featureKey,
order: loaded, resolvedFeatures.set(
}) featureProvider.clientFeatureProps.featureKey,
feature as ResolvedClientFeature<any>,
)
loaded++ loaded++
} }

View File

@@ -212,8 +212,8 @@ export const sanitizeClientFeatures = (
} }
export function sanitizeClientEditorConfig( export function sanitizeClientEditorConfig(
lexical: LexicalEditorConfig,
resolvedClientFeatureMap: ResolvedClientFeatureMap, resolvedClientFeatureMap: ResolvedClientFeatureMap,
lexical?: LexicalEditorConfig,
admin?: LexicalFieldAdminProps, admin?: LexicalFieldAdminProps,
): SanitizedClientEditorConfig { ): SanitizedClientEditorConfig {
return { return {

View File

@@ -126,18 +126,21 @@ export function setTargetLine(
/** /**
* Properly reset previous targetBlockElem styles * Properly reset previous targetBlockElem styles
*/ */
lastTargetBlock.elem.style.opacity = '' if (lastTargetBlock?.elem) {
lastTargetBlock.elem.style.opacity = ''
if (lastTargetBlock?.elem === targetBlockElem) { if (lastTargetBlock?.elem === targetBlockElem) {
if (isBelow) { if (isBelow) {
lastTargetBlock.elem.style.marginTop = '' lastTargetBlock.elem.style.marginTop = ''
} else {
lastTargetBlock.elem.style.marginBottom = ''
}
} else { } else {
lastTargetBlock.elem.style.marginBottom = '' lastTargetBlock.elem.style.marginBottom = ''
lastTargetBlock.elem.style.marginTop = ''
} }
} else {
lastTargetBlock.elem.style.marginBottom = ''
lastTargetBlock.elem.style.marginTop = ''
} }
animationTimer = 0 animationTimer = 0
return { return {
isBelow, isBelow,

View File

@@ -1,7 +1,10 @@
import type { EditorConfig as LexicalEditorConfig, SerializedEditorState } from 'lexical' import type { EditorConfig as LexicalEditorConfig, SerializedEditorState } from 'lexical'
import type { RichTextAdapter, RichTextFieldProps, SanitizedConfig } from 'payload' import type { RichTextAdapter, RichTextFieldProps, SanitizedConfig } from 'payload'
import type React from 'react'
import type {
BaseClientFeatureProps,
FeatureProviderProviderClient,
} from './features/typesClient.js'
import type { FeatureProviderServer } from './features/typesServer.js' import type { FeatureProviderServer } from './features/typesServer.js'
import type { SanitizedServerEditorConfig } from './lexical/config/types.js' import type { SanitizedServerEditorConfig } from './lexical/config/types.js'
@@ -78,7 +81,6 @@ export type AdapterProps = {
} }
export type GeneratedFeatureProviderComponent = { export type GeneratedFeatureProviderComponent = {
ClientFeature: React.ReactNode clientFeature: FeatureProviderProviderClient<any, any>
key: string clientFeatureProps: BaseClientFeatureProps<object>
order: number
} }

View File

@@ -1,7 +1,5 @@
import type React from 'react'
import type { import type {
ClientComponentProps, BaseClientFeatureProps,
ClientFeature, ClientFeature,
ClientFeatureProviderMap, ClientFeatureProviderMap,
FeatureProviderClient, FeatureProviderClient,
@@ -10,14 +8,11 @@ import type {
} from '../features/typesClient.js' } from '../features/typesClient.js'
import type { ClientEditorConfig } from '../lexical/config/types.js' import type { ClientEditorConfig } from '../lexical/config/types.js'
import { createClientComponent } from '../features/createClientComponent.js'
export type CreateClientFeatureArgs<UnSanitizedClientProps, ClientProps> = export type CreateClientFeatureArgs<UnSanitizedClientProps, ClientProps> =
| ((props: { | ((props: {
clientFunctions: Record<string, any>
/** unSanitizedEditorConfig.features, but mapped */ /** unSanitizedEditorConfig.features, but mapped */
featureProviderMap: ClientFeatureProviderMap featureProviderMap: ClientFeatureProviderMap
props: ClientComponentProps<UnSanitizedClientProps> props: BaseClientFeatureProps<UnSanitizedClientProps>
// other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here
resolvedFeatures: ResolvedClientFeatureMap resolvedFeatures: ResolvedClientFeatureMap
// unSanitized EditorConfig, // unSanitized EditorConfig,
@@ -30,7 +25,7 @@ export const createClientFeature: <
ClientProps = UnSanitizedClientProps, ClientProps = UnSanitizedClientProps,
>( >(
args: CreateClientFeatureArgs<UnSanitizedClientProps, ClientProps>, args: CreateClientFeatureArgs<UnSanitizedClientProps, ClientProps>,
) => React.FC<ClientComponentProps<ClientProps>> = (feature) => { ) => FeatureProviderProviderClient<UnSanitizedClientProps, ClientProps> = (feature) => {
const featureProviderProvideClient: FeatureProviderProviderClient<any, any> = (props) => { const featureProviderProvideClient: FeatureProviderProviderClient<any, any> = (props) => {
const featureProviderClient: Partial<FeatureProviderClient<any, any>> = { const featureProviderClient: Partial<FeatureProviderClient<any, any>> = {
clientFeatureProps: props, clientFeatureProps: props,
@@ -38,13 +33,11 @@ export const createClientFeature: <
if (typeof feature === 'function') { if (typeof feature === 'function') {
featureProviderClient.feature = ({ featureProviderClient.feature = ({
clientFunctions,
featureProviderMap, featureProviderMap,
resolvedFeatures, resolvedFeatures,
unSanitizedEditorConfig, unSanitizedEditorConfig,
}) => { }) => {
const toReturn = feature({ const toReturn = feature({
clientFunctions,
featureProviderMap, featureProviderMap,
props, props,
resolvedFeatures, resolvedFeatures,
@@ -72,5 +65,5 @@ export const createClientFeature: <
return featureProviderClient as FeatureProviderClient<any, any> return featureProviderClient as FeatureProviderClient<any, any>
} }
return createClientComponent(featureProviderProvideClient) return featureProviderProvideClient
} }

View File

@@ -4,6 +4,7 @@ import { getComponent } from '@payloadcms/ui/shared'
import { createClientFields } from '@payloadcms/ui/utilities/createClientConfig' import { createClientFields } from '@payloadcms/ui/utilities/createClientConfig'
import { deepCopyObjectSimple } from 'payload' import { deepCopyObjectSimple } from 'payload'
import type { FeatureProviderProviderClient } from '../features/typesClient.js'
import type { ResolvedServerFeatureMap } from '../features/typesServer.js' import type { ResolvedServerFeatureMap } from '../features/typesServer.js'
import type { GeneratedFeatureProviderComponent } from '../types.js' import type { GeneratedFeatureProviderComponent } from '../types.js'
@@ -104,37 +105,28 @@ export const getGenerateComponentMap =
} }
const ClientComponent = resolvedFeature.ClientFeature const ClientComponent = resolvedFeature.ClientFeature
const ResolvedClientComponent = getComponent({ const resolvedClientFeature = getComponent({
identifier: 'lexical-clientComponent', identifier: 'lexical-clientComponent',
importMap, importMap,
payloadComponent: ClientComponent, payloadComponent: ClientComponent,
}) })
const clientComponentProps = resolvedFeature.clientFeatureProps const featureProviderProviderClient =
resolvedClientFeature.Component as unknown as FeatureProviderProviderClient<any, any>
if (!ClientComponent) { if (!ClientComponent) {
return null return null
} }
const clientFeatureProps = resolvedFeature.clientFeatureProps ?? {}
clientFeatureProps.featureKey = resolvedFeature.key
clientFeatureProps.order = resolvedFeature.order
if (resolvedClientFeature.clientProps) {
clientFeatureProps.clientProps = resolvedClientFeature.clientProps
}
return { return {
ClientFeature: clientFeature: featureProviderProviderClient,
clientComponentProps && typeof clientComponentProps === 'object' ? ( clientFeatureProps,
<ResolvedClientComponent.Component
{...clientComponentProps}
featureKey={resolvedFeature.key}
key={resolvedFeature.key}
order={resolvedFeature.order}
{...(ResolvedClientComponent?.clientProps || {})}
/>
) : (
<ResolvedClientComponent.Component
featureKey={resolvedFeature.key}
key={resolvedFeature.key}
order={resolvedFeature.order}
{...(ResolvedClientComponent?.clientProps || {})}
/>
),
key: resolvedFeature.key,
order: resolvedFeature.order,
} as GeneratedFeatureProviderComponent } as GeneratedFeatureProviderComponent
}) })
.filter((feature) => feature !== null), .filter((feature) => feature !== null),

View File

@@ -1,6 +1,5 @@
import type { GitCommit, RawGitCommit } from 'changelogen' import type { GitCommit } from 'changelogen'
import chalk from 'chalk'
import { execSync } from 'child_process' import { execSync } from 'child_process'
import fse from 'fs-extra' import fse from 'fs-extra'
import minimist from 'minimist' import minimist from 'minimist'
@@ -63,9 +62,10 @@ export const updateChangelog = async (args: Args = {}): Promise<ChangelogResult>
const conventionalCommits = await getLatestCommits(fromVersion, toVersion) const conventionalCommits = await getLatestCommits(fromVersion, toVersion)
const sections: Record<'breaking' | 'feat' | 'fix', string[]> = { const sections: Record<'breaking' | 'feat' | 'fix' | 'perf', string[]> = {
feat: [], feat: [],
fix: [], fix: [],
perf: [],
breaking: [], breaking: [],
} }
@@ -75,7 +75,7 @@ export const updateChangelog = async (args: Args = {}): Promise<ChangelogResult>
sections.breaking.push(formatCommitForChangelog(c, true)) sections.breaking.push(formatCommitForChangelog(c, true))
} }
if (c.type === 'feat' || c.type === 'fix') { if (c.type === 'feat' || c.type === 'fix' || c.type === 'perf') {
sections[c.type].push(formatCommitForChangelog(c)) sections[c.type].push(formatCommitForChangelog(c))
} }
}) })
@@ -89,6 +89,9 @@ export const updateChangelog = async (args: Args = {}): Promise<ChangelogResult>
if (sections.feat.length) { if (sections.feat.length) {
changelog += `### 🚀 Features\n\n${sections.feat.join('\n')}\n\n` changelog += `### 🚀 Features\n\n${sections.feat.join('\n')}\n\n`
} }
if (sections.perf.length) {
changelog += `### ⚡ Performance\n\n${sections.perf.join('\n')}\n\n`
}
if (sections.fix.length) { if (sections.fix.length) {
changelog += `### 🐛 Bug Fixes\n\n${sections.fix.join('\n')}\n\n` changelog += `### 🐛 Bug Fixes\n\n${sections.fix.join('\n')}\n\n`
} }