From b6a8d1c46170f14f0b3c7fdaee21c49f114016ad Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 3 Sep 2024 12:48:41 -0400 Subject: [PATCH] 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 --- packages/richtext-lexical/src/cell/index.tsx | 136 ++++-------------- .../src/exports/client/index.ts | 1 - .../src/features/createClientComponent.tsx | 19 --- .../features/createFeaturePropComponent.tsx | 28 ---- .../slateToLexical/feature.client.tsx | 2 +- .../src/features/typesClient.ts | 9 +- .../src/features/typesServer.ts | 4 +- .../upload/client/component/index.tsx | 51 +++---- packages/richtext-lexical/src/field/index.tsx | 127 ++++------------ packages/richtext-lexical/src/index.ts | 2 +- .../src/lexical/config/client/loader.ts | 48 ++----- .../src/lexical/config/client/sanitize.ts | 2 +- .../DraggableBlockPlugin/setTargetLine.ts | 17 ++- packages/richtext-lexical/src/types.ts | 10 +- .../src/utilities/createClientFeature.ts | 15 +- .../src/utilities/generateComponentMap.tsx | 34 ++--- scripts/utils/updateChangelog.ts | 11 +- 17 files changed, 138 insertions(+), 378 deletions(-) delete mode 100644 packages/richtext-lexical/src/features/createClientComponent.tsx delete mode 100644 packages/richtext-lexical/src/features/createFeaturePropComponent.tsx diff --git a/packages/richtext-lexical/src/cell/index.tsx b/packages/richtext-lexical/src/cell/index.tsx index b00520bc21..4e4f894ce0 100644 --- a/packages/richtext-lexical/src/cell/index.tsx +++ b/packages/richtext-lexical/src/cell/index.tsx @@ -3,9 +3,9 @@ import type { EditorConfig as LexicalEditorConfig } from 'lexical' import type { CellComponentProps, RichTextFieldClient } from 'payload' import { createHeadlessEditor } from '@lexical/headless' -import { useClientFunctions, useTableCell } from '@payloadcms/ui' +import { useTableCell } from '@payloadcms/ui' 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 { SanitizedClientEditorConfig } from '../lexical/config/types.js' @@ -24,7 +24,7 @@ export const RichTextCell: React.FC< > = (props) => { const { admin, - field: { _schemaPath, richTextComponentMap }, + field: { richTextComponentMap }, lexicalEditorConfig, } = props @@ -32,90 +32,35 @@ export const RichTextCell: React.FC< const { cellData } = useTableCell() - const clientFunctions = useClientFunctions() - const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false) + const finalSanitizedEditorConfig = useMemo(() => { + const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get( + 'features', + ) as GeneratedFeatureProviderComponent[] - const [featureProviders, setFeatureProviders] = useState< - FeatureProviderClient[] - >([]) + const featureProvidersLocal: FeatureProviderClient[] = [] + for (const clientFeature of clientFeatures) { + featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps)) + } - const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] = - useState(null) + const finalLexicalEditorConfig = lexicalEditorConfig + ? lexicalEditorConfig + : defaultEditorLexicalConfig - const featureProviderComponents: GeneratedFeatureProviderComponent[] = ( - richTextComponentMap.get('features') as GeneratedFeatureProviderComponent[] - ).sort((a, b) => a.order - b.order) // order by order + const resolvedClientFeatures = loadClientFeatures({ + unSanitizedEditorConfig: { + features: featureProvidersLocal, + lexical: finalLexicalEditorConfig, + }, + }) - let featureProvidersAndComponentsToLoad = 0 // feature providers and components - for (const featureProvider of featureProviderComponents) { - const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) => - key.startsWith(`feature.${featureProvider.key}.components.`), - ) + return sanitizeClientEditorConfig(resolvedClientFeatures, finalLexicalEditorConfig) + }, [richTextComponentMap, lexicalEditorConfig]) - featureProvidersAndComponentsToLoad += 1 - featureProvidersAndComponentsToLoad += featureComponentKeys.length - } + finalSanitizedEditorConfig.admin = admin useEffect(() => { - if (!hasLoadedFeatures) { - const featureProvidersLocal: FeatureProviderClient[] = [] - 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 - if (dataToUse == null || !hasLoadedFeatures || !finalSanitizedEditorConfig) { + if (dataToUse == null || !finalSanitizedEditorConfig) { setPreview('') return } @@ -159,38 +104,7 @@ export const RichTextCell: React.FC< // Limiting the number of characters shown is done in a CSS rule setPreview(textContent) - }, [cellData, lexicalEditorConfig, hasLoadedFeatures, finalSanitizedEditorConfig]) - - if (!hasLoadedFeatures) { - return ( - - {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 ( - - {featureComponents?.length - ? featureComponents.map((FeatureComponent) => { - return FeatureComponent - }) - : null} - {featureProvider.ClientFeature} - - ) - })} - - ) - } + }, [cellData, finalSanitizedEditorConfig]) return {preview} } diff --git a/packages/richtext-lexical/src/exports/client/index.ts b/packages/richtext-lexical/src/exports/client/index.ts index 4e0dc6a87e..61b1543c8f 100644 --- a/packages/richtext-lexical/src/exports/client/index.ts +++ b/packages/richtext-lexical/src/exports/client/index.ts @@ -7,7 +7,6 @@ export { RichTextCell } from '../../cell/index.js' export { AlignFeatureClient } from '../../features/align/client/index.js' export { BlockquoteFeatureClient } from '../../features/blockquote/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 { TreeViewFeatureClient } from '../../features/debug/treeView/client/index.js' export { BoldFeatureClient } from '../../features/format/bold/feature.client.js' diff --git a/packages/richtext-lexical/src/features/createClientComponent.tsx b/packages/richtext-lexical/src/features/createClientComponent.tsx deleted file mode 100644 index 8f302018be..0000000000 --- a/packages/richtext-lexical/src/features/createClientComponent.tsx +++ /dev/null @@ -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 = ( - clientFeature: FeatureProviderProviderClient, -): React.FC> => { - return (props) => { - useLexicalFeature(props.featureKey, clientFeature(props)) - return null - } -} diff --git a/packages/richtext-lexical/src/features/createFeaturePropComponent.tsx b/packages/richtext-lexical/src/features/createFeaturePropComponent.tsx deleted file mode 100644 index 90cb50a245..0000000000 --- a/packages/richtext-lexical/src/features/createFeaturePropComponent.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client' - -import type React from 'react' - -import { useAddClientFunction, useFieldProps, useTableCell } from '@payloadcms/ui' - -const useLexicalFeatureProp = (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 = ( - prop: T, -): React.FC<{ componentKey: string; featureKey: string }> => { - return (props) => { - useLexicalFeatureProp(props.featureKey, props.componentKey, prop) - return null - } -} diff --git a/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx b/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx index 0970417fa3..43e0c673e0 100644 --- a/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx +++ b/packages/richtext-lexical/src/features/migrations/slateToLexical/feature.client.tsx @@ -3,7 +3,7 @@ import { createClientFeature } from '../../../utilities/createClientFeature.js' import { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js' -export const SlateToLexicalFeatureClient = createClientFeature(({ clientFunctions }) => { +export const SlateToLexicalFeatureClient = createClientFeature(() => { return { nodes: [UnknownConvertedNode], } diff --git a/packages/richtext-lexical/src/features/typesClient.ts b/packages/richtext-lexical/src/features/typesClient.ts index b2b0f0cf64..d39ec0e0fe 100644 --- a/packages/richtext-lexical/src/features/typesClient.ts +++ b/packages/richtext-lexical/src/features/typesClient.ts @@ -15,7 +15,7 @@ import type { ToolbarGroup } from './toolbars/types.js' export type FeatureProviderProviderClient< UnSanitizedClientFeatureProps = undefined, ClientFeatureProps = UnSanitizedClientFeatureProps, -> = (props: ClientComponentProps) => FeatureProviderClient +> = (props: BaseClientFeatureProps) => FeatureProviderClient /** * 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 */ - clientFeatureProps: ClientComponentProps + clientFeatureProps: BaseClientFeatureProps feature: | ((props: { - clientFunctions: Record /** unSanitizedEditorConfig.features, but mapped */ featureProviderMap: ClientFeatureProviderMap // 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 = { /** * Return props, to make it easy to retrieve passed in props to this Feature for the client if anyone wants to */ - sanitizedClientFeatureProps?: ClientComponentProps + sanitizedClientFeatureProps?: BaseClientFeatureProps slashMenu?: { /** * 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 = { } } -export type ClientComponentProps = ClientFeatureProps extends undefined +export type BaseClientFeatureProps = ClientFeatureProps extends undefined ? { featureKey: string order: number diff --git a/packages/richtext-lexical/src/features/typesServer.ts b/packages/richtext-lexical/src/features/typesServer.ts index 90331ac705..c0d797cbbd 100644 --- a/packages/richtext-lexical/src/features/typesServer.ts +++ b/packages/richtext-lexical/src/features/typesServer.ts @@ -26,7 +26,7 @@ import type { import type { ServerEditorConfig } from '../lexical/config/types.js' import type { AdapterProps } from '../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 = ({ context, @@ -282,7 +282,7 @@ export type NodeWithHooks = { } export type ServerFeature = { - ClientFeature?: PayloadComponent> + ClientFeature?: PayloadComponent> /** * This determines what props will be available on the Client. */ diff --git a/packages/richtext-lexical/src/features/upload/client/component/index.tsx b/packages/richtext-lexical/src/features/upload/client/component/index.tsx index a652b9f0c1..1496488fe4 100644 --- a/packages/richtext-lexical/src/features/upload/client/component/index.tsx +++ b/packages/richtext-lexical/src/features/upload/client/component/index.tsx @@ -28,7 +28,7 @@ import { } from 'lexical' 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 { UploadFeaturePropsClient } from '../feature.client.js' import type { UploadNode } from '../nodes/UploadNode.js' @@ -138,38 +138,41 @@ const Component: React.FC = (props) => { }, [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(() => { return mergeRegister( - editor.registerCommand(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW), + editor.registerCommand( + 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_BACKSPACE_COMMAND, $onDelete, COMMAND_PRIORITY_LOW), ) - }, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected, onClick]) + }, [clearSelection, editor, isSelected, nodeKey, $onDelete, setSelected]) const hasExtraFields = ( editorConfig?.resolvedFeatureMap?.get('upload') - ?.sanitizedClientFeatureProps as ClientComponentProps + ?.sanitizedClientFeatureProps as BaseClientFeatureProps ).collections?.[relatedCollection.slug]?.hasExtraFields const onExtraFieldsDrawerSubmit = useCallback( @@ -272,7 +275,7 @@ const Component: React.FC = (props) => { - {value && } + {value ? : null} {hasExtraFields ? ( = (props) => { field: { richTextComponentMap }, lexicalEditorConfig, } = props - const { schemaPath } = useFieldProps() - const clientFunctions = useClientFunctions() - const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false) - - const [featureProviders, setFeatureProviders] = useState< - FeatureProviderClient[] - >([]) const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] = useState(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(() => { - if (!hasLoadedFeatures) { - const featureProvidersLocal: FeatureProviderClient[] = [] - 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, - ), - ) - } + if (finalSanitizedEditorConfig) { + return } - }, [ - admin, - hasLoadedFeatures, - clientFunctions, - schemaPath, - featureProviderComponents.length, - featureProviders, - finalSanitizedEditorConfig, - lexicalEditorConfig, - featureProvidersAndComponentsToLoad, - ]) + const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get( + 'features', + ) as GeneratedFeatureProviderComponent[] - if (!hasLoadedFeatures) { - return ( - - {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 + const featureProvidersLocal: FeatureProviderClient[] = [] + for (const clientFeature of clientFeatures) { + featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps)) + } - return ( - - {featureComponents?.length - ? featureComponents.map((FeatureComponent) => { - return FeatureComponent - }) - : null} - {featureProvider.ClientFeature} - - ) - })} - + const finalLexicalEditorConfig = lexicalEditorConfig + ? lexicalEditorConfig + : defaultEditorLexicalConfig + + const resolvedClientFeatures = loadClientFeatures({ + unSanitizedEditorConfig: { + features: featureProvidersLocal, + lexical: finalLexicalEditorConfig, + }, + }) + + 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 ( }> diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index fad22d3a60..dab720a171 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -889,7 +889,7 @@ export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js export type { ToolbarGroup, ToolbarGroupItem } from './features/toolbars/types.js' export type { - ClientComponentProps, + BaseClientFeatureProps, ClientFeature, ClientFeatureProviderMap, FeatureProviderClient, diff --git a/packages/richtext-lexical/src/lexical/config/client/loader.ts b/packages/richtext-lexical/src/lexical/config/client/loader.ts index 2526a34fdc..cee6e7b325 100644 --- a/packages/richtext-lexical/src/lexical/config/client/loader.ts +++ b/packages/richtext-lexical/src/lexical/config/client/loader.ts @@ -2,7 +2,7 @@ import type { ClientFeatureProviderMap, - FeatureProviderClient, + ResolvedClientFeature, ResolvedClientFeatureMap, } from '../../../features/typesClient.js' import type { ClientEditorConfig } from '../types.js' @@ -12,12 +12,8 @@ import type { ClientEditorConfig } from '../types.js' * @param unSanitizedEditorConfig */ export function loadClientFeatures({ - clientFunctions, - schemaPath, unSanitizedEditorConfig, }: { - clientFunctions?: Record - schemaPath: string unSanitizedEditorConfig: ClientEditorConfig }): ResolvedClientFeatureMap { for (const featureProvider of unSanitizedEditorConfig.features) { @@ -37,50 +33,32 @@ export function loadClientFeatures({ (a, b) => a.clientFeatureProps.order - b.clientFeatureProps.order, ) - const featureProviderMap: ClientFeatureProviderMap = new Map( - unSanitizedEditorConfig.features.map( - (f) => - [f.clientFeatureProps.featureKey, f] as [string, FeatureProviderClient], - ), - ) + const featureProviderMap: ClientFeatureProviderMap = new Map() + for (const feature of unSanitizedEditorConfig.features) { + featureProviderMap.set(feature.clientFeatureProps.featureKey, feature) + } const resolvedFeatures: ResolvedClientFeatureMap = new Map() // Make sure all dependencies declared in the respective features exist let loaded = 0 for (const featureProvider of unSanitizedEditorConfig.features) { - /** - * Load relevant clientFunctions scoped to this feature and then pass them to the client feature - */ - const relevantClientFunctions: Record = {} - 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 = + const feature: Partial> = typeof featureProvider.feature === 'function' ? featureProvider.feature({ - clientFunctions: relevantClientFunctions, featureProviderMap, resolvedFeatures, unSanitizedEditorConfig, }) : featureProvider.feature - resolvedFeatures.set(featureProvider.clientFeatureProps.featureKey, { - ...feature, - key: featureProvider.clientFeatureProps.featureKey, - order: loaded, - }) + feature.key = featureProvider.clientFeatureProps.featureKey + feature.order = loaded + + resolvedFeatures.set( + featureProvider.clientFeatureProps.featureKey, + feature as ResolvedClientFeature, + ) loaded++ } diff --git a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts index 452ef10e80..06ae1102a2 100644 --- a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts +++ b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts @@ -212,8 +212,8 @@ export const sanitizeClientFeatures = ( } export function sanitizeClientEditorConfig( - lexical: LexicalEditorConfig, resolvedClientFeatureMap: ResolvedClientFeatureMap, + lexical?: LexicalEditorConfig, admin?: LexicalFieldAdminProps, ): SanitizedClientEditorConfig { return { diff --git a/packages/richtext-lexical/src/lexical/plugins/handles/DraggableBlockPlugin/setTargetLine.ts b/packages/richtext-lexical/src/lexical/plugins/handles/DraggableBlockPlugin/setTargetLine.ts index fbe4568a43..bd0221c2c7 100644 --- a/packages/richtext-lexical/src/lexical/plugins/handles/DraggableBlockPlugin/setTargetLine.ts +++ b/packages/richtext-lexical/src/lexical/plugins/handles/DraggableBlockPlugin/setTargetLine.ts @@ -126,18 +126,21 @@ export function setTargetLine( /** * Properly reset previous targetBlockElem styles */ - lastTargetBlock.elem.style.opacity = '' + if (lastTargetBlock?.elem) { + lastTargetBlock.elem.style.opacity = '' - if (lastTargetBlock?.elem === targetBlockElem) { - if (isBelow) { - lastTargetBlock.elem.style.marginTop = '' + if (lastTargetBlock?.elem === targetBlockElem) { + if (isBelow) { + lastTargetBlock.elem.style.marginTop = '' + } else { + lastTargetBlock.elem.style.marginBottom = '' + } } else { lastTargetBlock.elem.style.marginBottom = '' + lastTargetBlock.elem.style.marginTop = '' } - } else { - lastTargetBlock.elem.style.marginBottom = '' - lastTargetBlock.elem.style.marginTop = '' } + animationTimer = 0 return { isBelow, diff --git a/packages/richtext-lexical/src/types.ts b/packages/richtext-lexical/src/types.ts index 6141163967..fb812009f3 100644 --- a/packages/richtext-lexical/src/types.ts +++ b/packages/richtext-lexical/src/types.ts @@ -1,7 +1,10 @@ import type { EditorConfig as LexicalEditorConfig, SerializedEditorState } from 'lexical' 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 { SanitizedServerEditorConfig } from './lexical/config/types.js' @@ -78,7 +81,6 @@ export type AdapterProps = { } export type GeneratedFeatureProviderComponent = { - ClientFeature: React.ReactNode - key: string - order: number + clientFeature: FeatureProviderProviderClient + clientFeatureProps: BaseClientFeatureProps } diff --git a/packages/richtext-lexical/src/utilities/createClientFeature.ts b/packages/richtext-lexical/src/utilities/createClientFeature.ts index 9a75404e1f..fe2584019a 100644 --- a/packages/richtext-lexical/src/utilities/createClientFeature.ts +++ b/packages/richtext-lexical/src/utilities/createClientFeature.ts @@ -1,7 +1,5 @@ -import type React from 'react' - import type { - ClientComponentProps, + BaseClientFeatureProps, ClientFeature, ClientFeatureProviderMap, FeatureProviderClient, @@ -10,14 +8,11 @@ import type { } from '../features/typesClient.js' import type { ClientEditorConfig } from '../lexical/config/types.js' -import { createClientComponent } from '../features/createClientComponent.js' - export type CreateClientFeatureArgs = | ((props: { - clientFunctions: Record /** unSanitizedEditorConfig.features, but mapped */ featureProviderMap: ClientFeatureProviderMap - props: ClientComponentProps + props: BaseClientFeatureProps // other resolved features, which have been loaded before this one. All features declared in 'dependencies' should be available here resolvedFeatures: ResolvedClientFeatureMap // unSanitized EditorConfig, @@ -30,7 +25,7 @@ export const createClientFeature: < ClientProps = UnSanitizedClientProps, >( args: CreateClientFeatureArgs, -) => React.FC> = (feature) => { +) => FeatureProviderProviderClient = (feature) => { const featureProviderProvideClient: FeatureProviderProviderClient = (props) => { const featureProviderClient: Partial> = { clientFeatureProps: props, @@ -38,13 +33,11 @@ export const createClientFeature: < if (typeof feature === 'function') { featureProviderClient.feature = ({ - clientFunctions, featureProviderMap, resolvedFeatures, unSanitizedEditorConfig, }) => { const toReturn = feature({ - clientFunctions, featureProviderMap, props, resolvedFeatures, @@ -72,5 +65,5 @@ export const createClientFeature: < return featureProviderClient as FeatureProviderClient } - return createClientComponent(featureProviderProvideClient) + return featureProviderProvideClient } diff --git a/packages/richtext-lexical/src/utilities/generateComponentMap.tsx b/packages/richtext-lexical/src/utilities/generateComponentMap.tsx index 92247e47dc..b720a9f07d 100644 --- a/packages/richtext-lexical/src/utilities/generateComponentMap.tsx +++ b/packages/richtext-lexical/src/utilities/generateComponentMap.tsx @@ -4,6 +4,7 @@ import { getComponent } from '@payloadcms/ui/shared' import { createClientFields } from '@payloadcms/ui/utilities/createClientConfig' import { deepCopyObjectSimple } from 'payload' +import type { FeatureProviderProviderClient } from '../features/typesClient.js' import type { ResolvedServerFeatureMap } from '../features/typesServer.js' import type { GeneratedFeatureProviderComponent } from '../types.js' @@ -104,37 +105,28 @@ export const getGenerateComponentMap = } const ClientComponent = resolvedFeature.ClientFeature - const ResolvedClientComponent = getComponent({ + const resolvedClientFeature = getComponent({ identifier: 'lexical-clientComponent', importMap, payloadComponent: ClientComponent, }) - const clientComponentProps = resolvedFeature.clientFeatureProps + const featureProviderProviderClient = + resolvedClientFeature.Component as unknown as FeatureProviderProviderClient if (!ClientComponent) { return null } + const clientFeatureProps = resolvedFeature.clientFeatureProps ?? {} + clientFeatureProps.featureKey = resolvedFeature.key + clientFeatureProps.order = resolvedFeature.order + if (resolvedClientFeature.clientProps) { + clientFeatureProps.clientProps = resolvedClientFeature.clientProps + } + return { - ClientFeature: - clientComponentProps && typeof clientComponentProps === 'object' ? ( - - ) : ( - - ), - key: resolvedFeature.key, - order: resolvedFeature.order, + clientFeature: featureProviderProviderClient, + clientFeatureProps, } as GeneratedFeatureProviderComponent }) .filter((feature) => feature !== null), diff --git a/scripts/utils/updateChangelog.ts b/scripts/utils/updateChangelog.ts index 0a7ad5e4dc..d8d4e7b3d2 100755 --- a/scripts/utils/updateChangelog.ts +++ b/scripts/utils/updateChangelog.ts @@ -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 fse from 'fs-extra' import minimist from 'minimist' @@ -63,9 +62,10 @@ export const updateChangelog = async (args: Args = {}): Promise const conventionalCommits = await getLatestCommits(fromVersion, toVersion) - const sections: Record<'breaking' | 'feat' | 'fix', string[]> = { + const sections: Record<'breaking' | 'feat' | 'fix' | 'perf', string[]> = { feat: [], fix: [], + perf: [], breaking: [], } @@ -75,7 +75,7 @@ export const updateChangelog = async (args: Args = {}): Promise 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)) } }) @@ -89,6 +89,9 @@ export const updateChangelog = async (args: Args = {}): Promise if (sections.feat.length) { 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) { changelog += `### 🐛 Bug Fixes\n\n${sections.fix.join('\n')}\n\n` }