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:
@@ -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<SanitizedClientEditorConfig>(() => {
|
||||
const clientFeatures: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
|
||||
'features',
|
||||
) as GeneratedFeatureProviderComponent[]
|
||||
|
||||
const [featureProviders, setFeatureProviders] = useState<
|
||||
FeatureProviderClient<unknown, unknown>[]
|
||||
>([])
|
||||
const featureProvidersLocal: FeatureProviderClient<any, any>[] = []
|
||||
for (const clientFeature of clientFeatures) {
|
||||
featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps))
|
||||
}
|
||||
|
||||
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
|
||||
useState<SanitizedClientEditorConfig>(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<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
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}, [cellData, finalSanitizedEditorConfig])
|
||||
|
||||
return <span>{preview}</span>
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { ToolbarGroup } from './toolbars/types.js'
|
||||
export type FeatureProviderProviderClient<
|
||||
UnSanitizedClientFeatureProps = undefined,
|
||||
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
|
||||
@@ -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<UnSanitizedClientFeatureProps>
|
||||
clientFeatureProps: BaseClientFeatureProps<UnSanitizedClientFeatureProps>
|
||||
feature:
|
||||
| ((props: {
|
||||
clientFunctions: Record<string, any>
|
||||
/** 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<ClientFeatureProps> = {
|
||||
/**
|
||||
* 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?: {
|
||||
/**
|
||||
* 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
|
||||
order: number
|
||||
|
||||
@@ -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<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
|
||||
context,
|
||||
@@ -282,7 +282,7 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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<ElementProps> = (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<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_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<UploadFeaturePropsClient>
|
||||
?.sanitizedClientFeatureProps as BaseClientFeatureProps<UploadFeaturePropsClient>
|
||||
).collections?.[relatedCollection.slug]?.hasExtraFields
|
||||
|
||||
const onExtraFieldsDrawerSubmit = useCallback(
|
||||
@@ -272,7 +275,7 @@ const Component: React.FC<ElementProps> = (props) => {
|
||||
</DocumentDrawerToggler>
|
||||
</div>
|
||||
</div>
|
||||
{value && <DocumentDrawer onSave={updateUpload} />}
|
||||
{value ? <DocumentDrawer onSave={updateUpload} /> : null}
|
||||
{hasExtraFields ? (
|
||||
<FieldsDrawer
|
||||
data={fields}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { ShimmerEffect, useClientFunctions, useFieldProps } from '@payloadcms/ui'
|
||||
import { ShimmerEffect } from '@payloadcms/ui'
|
||||
import React, { lazy, Suspense, useEffect, useState } from 'react'
|
||||
|
||||
import type { FeatureProviderClient } from '../features/typesClient.js'
|
||||
@@ -21,117 +21,38 @@ export const RichTextField: React.FC<LexicalRichTextFieldProps> = (props) => {
|
||||
field: { richTextComponentMap },
|
||||
lexicalEditorConfig,
|
||||
} = props
|
||||
const { schemaPath } = useFieldProps()
|
||||
const clientFunctions = useClientFunctions()
|
||||
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
|
||||
|
||||
const [featureProviders, setFeatureProviders] = useState<
|
||||
FeatureProviderClient<unknown, unknown>[]
|
||||
>([])
|
||||
|
||||
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
|
||||
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(() => {
|
||||
if (!hasLoadedFeatures) {
|
||||
const featureProvidersLocal: FeatureProviderClient<unknown, unknown>[] = []
|
||||
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 (
|
||||
<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[] // TODO: Type better
|
||||
const featureProvidersLocal: FeatureProviderClient<any, any>[] = []
|
||||
for (const clientFeature of clientFeatures) {
|
||||
featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps))
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={featureProvider.key}>
|
||||
{featureComponents?.length
|
||||
? featureComponents.map((FeatureComponent) => {
|
||||
return FeatureComponent
|
||||
})
|
||||
: null}
|
||||
{featureProvider.ClientFeature}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</React.Fragment>
|
||||
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 (
|
||||
<Suspense fallback={<ShimmerEffect height="35vh" />}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, any>
|
||||
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<unknown, unknown>],
|
||||
),
|
||||
)
|
||||
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<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 =
|
||||
const feature: Partial<ResolvedClientFeature<any>> =
|
||||
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<any>,
|
||||
)
|
||||
|
||||
loaded++
|
||||
}
|
||||
|
||||
@@ -212,8 +212,8 @@ export const sanitizeClientFeatures = (
|
||||
}
|
||||
|
||||
export function sanitizeClientEditorConfig(
|
||||
lexical: LexicalEditorConfig,
|
||||
resolvedClientFeatureMap: ResolvedClientFeatureMap,
|
||||
lexical?: LexicalEditorConfig,
|
||||
admin?: LexicalFieldAdminProps,
|
||||
): SanitizedClientEditorConfig {
|
||||
return {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<any, any>
|
||||
clientFeatureProps: BaseClientFeatureProps<object>
|
||||
}
|
||||
|
||||
@@ -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<UnSanitizedClientProps, ClientProps> =
|
||||
| ((props: {
|
||||
clientFunctions: Record<string, any>
|
||||
/** unSanitizedEditorConfig.features, but mapped */
|
||||
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
|
||||
resolvedFeatures: ResolvedClientFeatureMap
|
||||
// unSanitized EditorConfig,
|
||||
@@ -30,7 +25,7 @@ export const createClientFeature: <
|
||||
ClientProps = UnSanitizedClientProps,
|
||||
>(
|
||||
args: CreateClientFeatureArgs<UnSanitizedClientProps, ClientProps>,
|
||||
) => React.FC<ClientComponentProps<ClientProps>> = (feature) => {
|
||||
) => FeatureProviderProviderClient<UnSanitizedClientProps, ClientProps> = (feature) => {
|
||||
const featureProviderProvideClient: FeatureProviderProviderClient<any, any> = (props) => {
|
||||
const featureProviderClient: Partial<FeatureProviderClient<any, any>> = {
|
||||
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<any, any>
|
||||
}
|
||||
|
||||
return createClientComponent(featureProviderProvideClient)
|
||||
return featureProviderProvideClient
|
||||
}
|
||||
|
||||
@@ -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<any, any>
|
||||
|
||||
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' ? (
|
||||
<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,
|
||||
clientFeature: featureProviderProviderClient,
|
||||
clientFeatureProps,
|
||||
} as GeneratedFeatureProviderComponent
|
||||
})
|
||||
.filter((feature) => feature !== null),
|
||||
|
||||
@@ -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<ChangelogResult>
|
||||
|
||||
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<ChangelogResult>
|
||||
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<ChangelogResult>
|
||||
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`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user