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 { 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 [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
useState<SanitizedClientEditorConfig>(null)
const featureProviderComponents: GeneratedFeatureProviderComponent[] = (
richTextComponentMap.get('features') as GeneratedFeatureProviderComponent[]
).sort((a, b) => a.order - b.order) // order by order
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.`),
)
featureProvidersAndComponentsToLoad += 1
featureProvidersAndComponentsToLoad += featureComponentKeys.length
const featureProvidersLocal: FeatureProviderClient<any, any>[] = []
for (const clientFeature of clientFeatures) {
featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps))
}
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 finalLexicalEditorConfig = lexicalEditorConfig
? lexicalEditorConfig
: defaultEditorLexicalConfig
const resolvedClientFeatures = loadClientFeatures({
clientFunctions,
schemaPath: _schemaPath,
unSanitizedEditorConfig: {
features: featureProvidersLocal,
lexical: lexicalEditorConfig,
lexical: finalLexicalEditorConfig,
},
})
setFinalSanitizedEditorConfig(
sanitizeClientEditorConfig(
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
resolvedClientFeatures,
admin,
),
)
}
}
}, [
admin,
featureProviderComponents,
hasLoadedFeatures,
clientFunctions,
_schemaPath,
featureProviderComponents.length,
featureProviders,
finalSanitizedEditorConfig,
lexicalEditorConfig,
richTextComponentMap,
featureProvidersAndComponentsToLoad,
])
return sanitizeClientEditorConfig(resolvedClientFeatures, finalLexicalEditorConfig)
}, [richTextComponentMap, lexicalEditorConfig])
finalSanitizedEditorConfig.admin = admin
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>
}

View File

@@ -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'

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 { UnknownConvertedNode } from './nodes/unknownConvertedNode/index.js'
export const SlateToLexicalFeatureClient = createClientFeature(({ clientFunctions }) => {
export const SlateToLexicalFeatureClient = createClientFeature(() => {
return {
nodes: [UnknownConvertedNode],
}

View File

@@ -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

View File

@@ -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.
*/

View File

@@ -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,10 +138,17 @@ const Component: React.FC<ElementProps> = (props) => {
},
[isSelected, nodeKey],
)
const onClick = useCallback(
useEffect(() => {
return mergeRegister(
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.target === uploadRef.current ||
uploadRef.current?.contains(event.target as Node)
) {
if (event.shiftKey) {
setSelected(!isSelected)
} else {
@@ -155,21 +162,17 @@ const Component: React.FC<ElementProps> = (props) => {
return false
},
[isSelected, setSelected, clearSelection],
)
useEffect(() => {
return mergeRegister(
editor.registerCommand<MouseEvent>(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW),
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}

View File

@@ -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(
useEffect(() => {
if (finalSanitizedEditorConfig) {
return
}
const clientFeatures: 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
const featureProvidersLocal: FeatureProviderClient<any, any>[] = []
for (const clientFeature of clientFeatures) {
featureProvidersLocal.push(clientFeature.clientFeature(clientFeature.clientFeatureProps))
}
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 finalLexicalEditorConfig = lexicalEditorConfig
? lexicalEditorConfig
: defaultEditorLexicalConfig
const resolvedClientFeatures = loadClientFeatures({
clientFunctions,
schemaPath,
unSanitizedEditorConfig: {
features: featureProvidersLocal,
lexical: lexicalEditorConfig,
lexical: finalLexicalEditorConfig,
},
})
setFinalSanitizedEditorConfig(
sanitizeClientEditorConfig(
lexicalEditorConfig ? lexicalEditorConfig : defaultEditorLexicalConfig,
resolvedClientFeatures,
admin,
),
sanitizeClientEditorConfig(resolvedClientFeatures, finalLexicalEditorConfig, admin),
)
}
}
}, [
admin,
hasLoadedFeatures,
clientFunctions,
schemaPath,
featureProviderComponents.length,
featureProviders,
finalSanitizedEditorConfig,
lexicalEditorConfig,
featureProvidersAndComponentsToLoad,
])
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
return (
<React.Fragment key={featureProvider.key}>
{featureComponents?.length
? featureComponents.map((FeatureComponent) => {
return FeatureComponent
})
: null}
{featureProvider.ClientFeature}
</React.Fragment>
)
})}
</React.Fragment>
)
}
}, [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" />}>

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 {
ClientComponentProps,
BaseClientFeatureProps,
ClientFeature,
ClientFeatureProviderMap,
FeatureProviderClient,

View File

@@ -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++
}

View File

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

View File

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

View File

@@ -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>
}

View File

@@ -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
}

View File

@@ -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),

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 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`
}