fix(richtext-*): fix client features were not loaded properly, improve performance of LexicalProvider, slate cell component was non-functional, support richtext adapter Cell RSCs (#6573)
This commit is contained in:
@@ -12,13 +12,6 @@ import type {
|
|||||||
export type RowData = Record<string, any>
|
export type RowData = Record<string, any>
|
||||||
|
|
||||||
export type CellComponentProps = {
|
export type CellComponentProps = {
|
||||||
/**
|
|
||||||
* A custom component to override the default cell component. If this is not set, the React component will be
|
|
||||||
* taken from cellComponents based on the field type.
|
|
||||||
*
|
|
||||||
* This is used to provide the RichText cell component for the RichText field.
|
|
||||||
*/
|
|
||||||
CellComponentOverride?: React.ReactNode
|
|
||||||
blocks?: {
|
blocks?: {
|
||||||
labels: BlockField['labels']
|
labels: BlockField['labels']
|
||||||
slug: string
|
slug: string
|
||||||
@@ -39,6 +32,7 @@ export type CellComponentProps = {
|
|||||||
options?: SelectField['options']
|
options?: SelectField['options']
|
||||||
relationTo?: RelationshipField['relationTo']
|
relationTo?: RelationshipField['relationTo']
|
||||||
richTextComponentMap?: Map<string, React.ReactNode> // any should be MappedField
|
richTextComponentMap?: Map<string, React.ReactNode> // any should be MappedField
|
||||||
|
schemaPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DefaultCellComponentProps<T = any> = CellComponentProps & {
|
export type DefaultCellComponentProps<T = any> = CellComponentProps & {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { EditorConfig as LexicalEditorConfig } from 'lexical'
|
import type { EditorConfig as LexicalEditorConfig } from 'lexical'
|
||||||
|
import type { CellComponentProps } from 'payload/types'
|
||||||
|
|
||||||
import { createHeadlessEditor } from '@lexical/headless'
|
import { createHeadlessEditor } from '@lexical/headless'
|
||||||
import { useTableCell } from '@payloadcms/ui/elements/Table'
|
import { useTableCell } from '@payloadcms/ui/elements/Table'
|
||||||
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
|
|
||||||
import { useClientFunctions } from '@payloadcms/ui/providers/ClientFunction'
|
import { useClientFunctions } from '@payloadcms/ui/providers/ClientFunction'
|
||||||
import { $getRoot } from 'lexical'
|
import { $getRoot } from 'lexical'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
@@ -17,16 +17,20 @@ import { loadClientFeatures } from '../field/lexical/config/client/loader.js'
|
|||||||
import { sanitizeClientEditorConfig } from '../field/lexical/config/client/sanitize.js'
|
import { sanitizeClientEditorConfig } from '../field/lexical/config/client/sanitize.js'
|
||||||
import { getEnabledNodes } from '../field/lexical/nodes/index.js'
|
import { getEnabledNodes } from '../field/lexical/nodes/index.js'
|
||||||
|
|
||||||
export const RichTextCell: React.FC<{
|
export const RichTextCell: React.FC<
|
||||||
admin?: LexicalFieldAdminProps
|
CellComponentProps & {
|
||||||
lexicalEditorConfig: LexicalEditorConfig
|
admin?: LexicalFieldAdminProps
|
||||||
}> = (props) => {
|
lexicalEditorConfig: LexicalEditorConfig
|
||||||
const { admin, lexicalEditorConfig } = props
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const { admin, lexicalEditorConfig, richTextComponentMap } = props
|
||||||
|
|
||||||
const [preview, setPreview] = React.useState('Loading...')
|
const [preview, setPreview] = React.useState('Loading...')
|
||||||
const { schemaPath } = useFieldProps()
|
|
||||||
|
|
||||||
const { cellData, richTextComponentMap } = useTableCell()
|
const {
|
||||||
|
cellData,
|
||||||
|
cellProps: { schemaPath },
|
||||||
|
} = useTableCell()
|
||||||
|
|
||||||
const clientFunctions = useClientFunctions()
|
const clientFunctions = useClientFunctions()
|
||||||
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
|
const [hasLoadedFeatures, setHasLoadedFeatures] = useState(false)
|
||||||
@@ -36,21 +40,24 @@ export const RichTextCell: React.FC<{
|
|||||||
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
|
const [finalSanitizedEditorConfig, setFinalSanitizedEditorConfig] =
|
||||||
useState<SanitizedClientEditorConfig>(null)
|
useState<SanitizedClientEditorConfig>(null)
|
||||||
|
|
||||||
let featureProviderComponents: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
|
const featureProviderComponents: GeneratedFeatureProviderComponent[] = (
|
||||||
'features',
|
richTextComponentMap.get('features') as GeneratedFeatureProviderComponent[]
|
||||||
) as GeneratedFeatureProviderComponent[] // TODO: Type better
|
).sort((a, b) => a.order - b.order) // order by order
|
||||||
// order by order
|
|
||||||
featureProviderComponents = featureProviderComponents.sort((a, b) => a.order - b.order)
|
|
||||||
|
|
||||||
const featureComponentsWithFeaturesLength =
|
let featureProvidersAndComponentsToLoad = 0 // feature providers and components
|
||||||
Array.from(richTextComponentMap.keys()).filter(
|
for (const featureProvider of featureProviderComponents) {
|
||||||
(key) => key.startsWith(`feature.`) && !key.includes('.fields.'),
|
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
|
||||||
).length + featureProviderComponents.length
|
key.startsWith(`feature.${featureProvider.key}.components.`),
|
||||||
|
)
|
||||||
|
|
||||||
|
featureProvidersAndComponentsToLoad += 1
|
||||||
|
featureProvidersAndComponentsToLoad += featureComponentKeys.length
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasLoadedFeatures) {
|
if (!hasLoadedFeatures) {
|
||||||
const featureProvidersLocal: FeatureProviderClient<unknown>[] = []
|
const featureProvidersLocal: FeatureProviderClient<unknown>[] = []
|
||||||
let featureProvidersAndComponentsLoaded = 0
|
let featureProvidersAndComponentsLoaded = 0 // feature providers and components only
|
||||||
|
|
||||||
Object.entries(clientFunctions).forEach(([key, plugin]) => {
|
Object.entries(clientFunctions).forEach(([key, plugin]) => {
|
||||||
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
|
if (key.startsWith(`lexicalFeature.${schemaPath}.`)) {
|
||||||
@@ -61,7 +68,7 @@ export const RichTextCell: React.FC<{
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (featureProvidersAndComponentsLoaded === featureComponentsWithFeaturesLength) {
|
if (featureProvidersAndComponentsLoaded === featureProvidersAndComponentsToLoad) {
|
||||||
setFeatureProviders(featureProvidersLocal)
|
setFeatureProviders(featureProvidersLocal)
|
||||||
setHasLoadedFeatures(true)
|
setHasLoadedFeatures(true)
|
||||||
|
|
||||||
@@ -89,6 +96,7 @@ export const RichTextCell: React.FC<{
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
admin,
|
admin,
|
||||||
|
featureProviderComponents,
|
||||||
hasLoadedFeatures,
|
hasLoadedFeatures,
|
||||||
clientFunctions,
|
clientFunctions,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
@@ -96,10 +104,14 @@ export const RichTextCell: React.FC<{
|
|||||||
featureProviders,
|
featureProviders,
|
||||||
finalSanitizedEditorConfig,
|
finalSanitizedEditorConfig,
|
||||||
lexicalEditorConfig,
|
lexicalEditorConfig,
|
||||||
featureComponentsWithFeaturesLength,
|
richTextComponentMap,
|
||||||
|
featureProvidersAndComponentsToLoad,
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!hasLoadedFeatures) {
|
||||||
|
return
|
||||||
|
}
|
||||||
let dataToUse = cellData
|
let dataToUse = cellData
|
||||||
if (dataToUse == null || !hasLoadedFeatures || !finalSanitizedEditorConfig) {
|
if (dataToUse == null || !hasLoadedFeatures || !finalSanitizedEditorConfig) {
|
||||||
setPreview('')
|
setPreview('')
|
||||||
@@ -156,6 +168,7 @@ export const RichTextCell: React.FC<{
|
|||||||
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
|
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
|
||||||
key.startsWith(`feature.${featureProvider.key}.components.`),
|
key.startsWith(`feature.${featureProvider.key}.components.`),
|
||||||
)
|
)
|
||||||
|
|
||||||
const featureComponents: React.ReactNode[] = featureComponentKeys.map((key) => {
|
const featureComponents: React.ReactNode[] = featureComponentKeys.map((key) => {
|
||||||
return richTextComponentMap.get(key)
|
return richTextComponentMap.get(key)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
|
import { useTableCell } from '@payloadcms/ui/elements/Table'
|
||||||
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
|
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
|
||||||
import { useAddClientFunction } from '@payloadcms/ui/providers/ClientFunction'
|
import { useAddClientFunction } from '@payloadcms/ui/providers/ClientFunction'
|
||||||
|
|
||||||
const useLexicalFeatureProp = <T,>(featureKey: string, componentKey: string, prop: T) => {
|
const useLexicalFeatureProp = <T,>(featureKey: string, componentKey: string, prop: T) => {
|
||||||
const { schemaPath } = useFieldProps()
|
const { schemaPath: schemaPathFromFieldProps } = useFieldProps()
|
||||||
|
const tableCell = useTableCell()
|
||||||
|
|
||||||
|
const schemaPathFromCellProps = tableCell?.cellProps?.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(
|
useAddClientFunction(
|
||||||
`lexicalFeature.${schemaPath}.${featureKey}.components.${componentKey}`,
|
`lexicalFeature.${schemaPath}.${featureKey}.components.${componentKey}`,
|
||||||
|
|||||||
@@ -39,14 +39,19 @@ export const RichTextField: React.FC<
|
|||||||
|
|
||||||
let featureProviderComponents: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
|
let featureProviderComponents: GeneratedFeatureProviderComponent[] = richTextComponentMap.get(
|
||||||
'features',
|
'features',
|
||||||
) as GeneratedFeatureProviderComponent[] // TODO: Type better
|
) as GeneratedFeatureProviderComponent[]
|
||||||
// order by order
|
// order by order
|
||||||
featureProviderComponents = featureProviderComponents.sort((a, b) => a.order - b.order)
|
featureProviderComponents = featureProviderComponents.sort((a, b) => a.order - b.order)
|
||||||
|
|
||||||
const featureComponentsWithFeaturesLength =
|
let featureProvidersAndComponentsToLoad = 0 // feature providers and components
|
||||||
Array.from(richTextComponentMap.keys()).filter(
|
for (const featureProvider of featureProviderComponents) {
|
||||||
(key) => key.startsWith(`feature.`) && !key.includes('.fields.'),
|
const featureComponentKeys = Array.from(richTextComponentMap.keys()).filter((key) =>
|
||||||
).length + featureProviderComponents.length
|
key.startsWith(`feature.${featureProvider.key}.components.`),
|
||||||
|
)
|
||||||
|
|
||||||
|
featureProvidersAndComponentsToLoad += 1
|
||||||
|
featureProvidersAndComponentsToLoad += featureComponentKeys.length
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasLoadedFeatures) {
|
if (!hasLoadedFeatures) {
|
||||||
@@ -62,7 +67,7 @@ export const RichTextField: React.FC<
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (featureProvidersAndComponentsLoaded === featureComponentsWithFeaturesLength) {
|
if (featureProvidersAndComponentsLoaded === featureProvidersAndComponentsToLoad) {
|
||||||
setFeatureProviders(featureProvidersLocal)
|
setFeatureProviders(featureProvidersLocal)
|
||||||
setHasLoadedFeatures(true)
|
setHasLoadedFeatures(true)
|
||||||
|
|
||||||
@@ -97,7 +102,7 @@ export const RichTextField: React.FC<
|
|||||||
featureProviders,
|
featureProviders,
|
||||||
finalSanitizedEditorConfig,
|
finalSanitizedEditorConfig,
|
||||||
lexicalEditorConfig,
|
lexicalEditorConfig,
|
||||||
featureComponentsWithFeaturesLength,
|
featureProvidersAndComponentsToLoad,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!hasLoadedFeatures) {
|
if (!hasLoadedFeatures) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { LexicalEditor } from 'lexical'
|
|||||||
|
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer.js'
|
import { LexicalComposer } from '@lexical/react/LexicalComposer.js'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
import type { SanitizedClientEditorConfig } from './config/types.js'
|
import type { SanitizedClientEditorConfig } from './config/types.js'
|
||||||
|
|
||||||
@@ -29,19 +30,47 @@ export type LexicalProviderProps = {
|
|||||||
value: SerializedEditorState
|
value: SerializedEditorState
|
||||||
}
|
}
|
||||||
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
||||||
const { editorConfig, fieldProps, onChange, path, readOnly } = props
|
const { editorConfig, fieldProps, value, onChange, path, readOnly } = props
|
||||||
let { value } = props
|
|
||||||
const parentContext = useEditorConfigContext()
|
const parentContext = useEditorConfigContext()
|
||||||
|
|
||||||
const editorContainerRef = React.useRef<HTMLDivElement>(null)
|
const editorContainerRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const [initialConfig, setInitialConfig] = React.useState<InitialConfigType | null>(null)
|
|
||||||
|
|
||||||
// set lexical config in useEffect: // TODO: Is this the most performant way to do this? Prob not
|
const processedValue = useMemo(() => {
|
||||||
React.useEffect(() => {
|
let processed = value
|
||||||
const newInitialConfig: InitialConfigType = {
|
if (editorConfig?.features?.hooks?.load?.length) {
|
||||||
|
editorConfig.features.hooks.load.forEach((hook) => {
|
||||||
|
processed = hook({ incomingEditorState: processed })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return processed
|
||||||
|
}, [editorConfig, value])
|
||||||
|
|
||||||
|
// useMemo for the initialConfig that depends on readOnly and processedValue
|
||||||
|
const initialConfig = useMemo<InitialConfigType>(() => {
|
||||||
|
if (processedValue && typeof processedValue !== 'object') {
|
||||||
|
throw new Error(
|
||||||
|
'The value passed to the Lexical editor is not an object. This is not supported. Please remove the data from the field and start again. This is the value that was passed in: ' +
|
||||||
|
JSON.stringify(processedValue),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedValue && Array.isArray(processedValue) && !('root' in processedValue)) {
|
||||||
|
throw new Error(
|
||||||
|
'You have tried to pass in data from the old, Slate editor, to the new, Lexical editor. This is not supported. There is no automatic conversion from Slate to Lexical data available yet (coming soon). Please remove the data from the field and start again.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedValue && 'jsonContent' in processedValue) {
|
||||||
|
throw new Error(
|
||||||
|
'You have tried to pass in data from payload-plugin-lexical. This is not supported. The data structure has changed in this editor, compared to the plugin, and there is no automatic conversion available yet (coming soon). Please remove the data from the field and start again.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
editable: readOnly !== true,
|
editable: readOnly !== true,
|
||||||
editorState: value != null ? JSON.stringify(value) : undefined,
|
editorState: processedValue != null ? JSON.stringify(processedValue) : undefined,
|
||||||
namespace: editorConfig.lexical.namespace,
|
namespace: editorConfig.lexical.namespace,
|
||||||
nodes: [...getEnabledNodes({ editorConfig })],
|
nodes: [...getEnabledNodes({ editorConfig })],
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
@@ -49,33 +78,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
|
|||||||
},
|
},
|
||||||
theme: editorConfig.lexical.theme,
|
theme: editorConfig.lexical.theme,
|
||||||
}
|
}
|
||||||
setInitialConfig(newInitialConfig)
|
}, [editorConfig, processedValue, readOnly])
|
||||||
}, [editorConfig, readOnly, value])
|
|
||||||
|
|
||||||
if (editorConfig?.features?.hooks?.load?.length) {
|
|
||||||
editorConfig.features.hooks.load.forEach((hook) => {
|
|
||||||
value = hook({ incomingEditorState: value })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && typeof value !== 'object') {
|
|
||||||
throw new Error(
|
|
||||||
'The value passed to the Lexical editor is not an object. This is not supported. Please remove the data from the field and start again. This is the value that was passed in: ' +
|
|
||||||
JSON.stringify(value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && Array.isArray(value) && !('root' in value)) {
|
|
||||||
throw new Error(
|
|
||||||
'You have tried to pass in data from the old, Slate editor, to the new, Lexical editor. This is not supported. There is no automatic conversion from Slate to Lexical data available yet (coming soon). Please remove the data from the field and start again.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && 'jsonContent' in value) {
|
|
||||||
throw new Error(
|
|
||||||
'You have tried to pass in data from payload-plugin-lexical. This is not supported. The data structure has changed in this editor, compared to the plugin, and there is no automatic conversion available yet (coming soon). Please remove the data from the field and start again.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!initialConfig) {
|
if (!initialConfig) {
|
||||||
return <p>Loading...</p>
|
return <p>Loading...</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useTableCell } from '@payloadcms/ui/elements/Table'
|
||||||
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
|
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
|
||||||
import { useAddClientFunction } from '@payloadcms/ui/providers/ClientFunction'
|
import { useAddClientFunction } from '@payloadcms/ui/providers/ClientFunction'
|
||||||
|
|
||||||
@@ -9,7 +10,12 @@ export const useLexicalFeature = <ClientFeatureProps,>(
|
|||||||
featureKey: string,
|
featureKey: string,
|
||||||
feature: FeatureProviderClient<ClientFeatureProps>,
|
feature: FeatureProviderClient<ClientFeatureProps>,
|
||||||
) => {
|
) => {
|
||||||
const { schemaPath } = useFieldProps()
|
const { schemaPath: schemaPathFromFieldProps } = useFieldProps()
|
||||||
|
const tableCell = useTableCell()
|
||||||
|
|
||||||
|
const schemaPathFromCellProps = tableCell?.cellProps?.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}`, feature)
|
useAddClientFunction(`lexicalFeature.${schemaPath}.${featureKey}`, feature)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { DefaultCellComponentProps } from 'payload/types'
|
import type { DefaultCellComponentProps } from 'payload/types'
|
||||||
|
|
||||||
|
import { useTableCell } from '@payloadcms/ui/elements/Table'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export const RichTextCell: React.FC<DefaultCellComponentProps<any[]>> = ({ cellData }) => {
|
export const RichTextCell: React.FC<DefaultCellComponentProps<any[]>> = () => {
|
||||||
|
const { cellData } = useTableCell()
|
||||||
const flattenedText = cellData?.map((i) => i?.children?.map((c) => c.text)).join(' ')
|
const flattenedText = cellData?.map((i) => i?.children?.map((c) => c.text)).join(' ')
|
||||||
|
|
||||||
// Limiting the number of characters shown is done in a CSS rule
|
// Limiting the number of characters shown is done in a CSS rule
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import type { DefaultCellComponentProps } from 'payload/types'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export const TextareaCell: React.FC<DefaultCellComponentProps<string>> = ({ cellData }) => {
|
export const TextareaCell: React.FC<DefaultCellComponentProps<string>> = ({ cellData }) => {
|
||||||
const textToShow = cellData?.length > 100 ? `${cellData.substr(0, 100)}\u2026` : cellData
|
const textToShow = cellData?.length > 100 ? `${cellData.substring(0, 100)}\u2026` : cellData
|
||||||
return <span>{textToShow}</span>
|
return <span>{textToShow}</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getTranslation } from '@payloadcms/translations'
|
|||||||
|
|
||||||
import { useConfig } from '../../../providers/Config/index.js'
|
import { useConfig } from '../../../providers/Config/index.js'
|
||||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||||
import { TableCellProvider, useTableCell } from '../TableCellProvider/index.js'
|
import { useTableCell } from '../TableCellProvider/index.js'
|
||||||
import { CodeCell } from './fields/Code/index.js'
|
import { CodeCell } from './fields/Code/index.js'
|
||||||
import { cellComponents } from './fields/index.js'
|
import { cellComponents } from './fields/index.js'
|
||||||
|
|
||||||
@@ -17,13 +17,11 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.
|
|||||||
export const DefaultCell: React.FC<CellComponentProps> = (props) => {
|
export const DefaultCell: React.FC<CellComponentProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
CellComponentOverride,
|
|
||||||
className: classNameFromProps,
|
className: classNameFromProps,
|
||||||
fieldType,
|
fieldType,
|
||||||
isFieldAffectingData,
|
isFieldAffectingData,
|
||||||
label,
|
label,
|
||||||
onClick: onClickFromProps,
|
onClick: onClickFromProps,
|
||||||
richTextComponentMap,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
@@ -77,7 +75,13 @@ export const DefaultCell: React.FC<CellComponentProps> = (props) => {
|
|||||||
if (name === 'id') {
|
if (name === 'id') {
|
||||||
return (
|
return (
|
||||||
<WrapElement {...wrapElementProps}>
|
<WrapElement {...wrapElementProps}>
|
||||||
<CodeCell cellData={`ID: ${cellData}`} name={name} nowrap rowData={rowData} />
|
<CodeCell
|
||||||
|
cellData={`ID: ${cellData}`}
|
||||||
|
name={name}
|
||||||
|
nowrap
|
||||||
|
rowData={rowData}
|
||||||
|
schemaPath={cellContext?.cellProps?.schemaPath}
|
||||||
|
/>
|
||||||
</WrapElement>
|
</WrapElement>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -85,15 +89,9 @@ export const DefaultCell: React.FC<CellComponentProps> = (props) => {
|
|||||||
const DefaultCellComponent: React.FC<DefaultCellComponentProps> =
|
const DefaultCellComponent: React.FC<DefaultCellComponentProps> =
|
||||||
typeof cellData !== 'undefined' && cellComponents[fieldType]
|
typeof cellData !== 'undefined' && cellComponents[fieldType]
|
||||||
|
|
||||||
let CellComponent: React.ReactNode =
|
let CellComponent: React.ReactNode = null
|
||||||
cellData &&
|
|
||||||
(CellComponentOverride ? ( // CellComponentOverride is used for richText
|
|
||||||
<TableCellProvider richTextComponentMap={richTextComponentMap}>
|
|
||||||
{CellComponentOverride}
|
|
||||||
</TableCellProvider>
|
|
||||||
) : null)
|
|
||||||
|
|
||||||
if (!CellComponent && DefaultCellComponent) {
|
if (DefaultCellComponent) {
|
||||||
CellComponent = (
|
CellComponent = (
|
||||||
<DefaultCellComponent
|
<DefaultCellComponent
|
||||||
cellData={cellData}
|
cellData={cellData}
|
||||||
@@ -102,7 +100,7 @@ export const DefaultCell: React.FC<CellComponentProps> = (props) => {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (!CellComponent && !DefaultCellComponent) {
|
} else if (!DefaultCellComponent) {
|
||||||
// DefaultCellComponent does not exist for certain field types like `text`
|
// DefaultCellComponent does not exist for certain field types like `text`
|
||||||
if (customCellContext.uploadConfig && isFieldAffectingData && name === 'filename') {
|
if (customCellContext.uploadConfig && isFieldAffectingData && name === 'filename') {
|
||||||
const FileCellComponent = cellComponents.File
|
const FileCellComponent = cellComponents.File
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export type ITableCellContext = {
|
|||||||
cellProps?: Partial<CellComponentProps>
|
cellProps?: Partial<CellComponentProps>
|
||||||
columnIndex?: number
|
columnIndex?: number
|
||||||
customCellContext: DefaultCellComponentProps['customCellContext']
|
customCellContext: DefaultCellComponentProps['customCellContext']
|
||||||
richTextComponentMap?: DefaultCellComponentProps['richTextComponentMap']
|
|
||||||
rowData: DefaultCellComponentProps['rowData']
|
rowData: DefaultCellComponentProps['rowData']
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,18 +19,9 @@ export const TableCellProvider: React.FC<{
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
columnIndex?: number
|
columnIndex?: number
|
||||||
customCellContext?: DefaultCellComponentProps['customCellContext']
|
customCellContext?: DefaultCellComponentProps['customCellContext']
|
||||||
richTextComponentMap?: DefaultCellComponentProps['richTextComponentMap']
|
|
||||||
rowData?: DefaultCellComponentProps['rowData']
|
rowData?: DefaultCellComponentProps['rowData']
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const {
|
const { cellData, cellProps, children, columnIndex, customCellContext, rowData } = props
|
||||||
cellData,
|
|
||||||
cellProps,
|
|
||||||
children,
|
|
||||||
columnIndex,
|
|
||||||
customCellContext,
|
|
||||||
richTextComponentMap,
|
|
||||||
rowData,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const contextToInherit = useTableCell()
|
const contextToInherit = useTableCell()
|
||||||
|
|
||||||
@@ -44,7 +34,6 @@ export const TableCellProvider: React.FC<{
|
|||||||
customCellContext,
|
customCellContext,
|
||||||
rowData,
|
rowData,
|
||||||
...contextToInherit,
|
...contextToInherit,
|
||||||
richTextComponentMap,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { serverProps } from 'payload/config'
|
import { serverProps } from 'payload/config'
|
||||||
import { deepMerge } from 'payload/utilities'
|
import { deepMerge, isReactServerComponentOrFunction } from 'payload/utilities'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,23 +19,30 @@ import React from 'react'
|
|||||||
* // <OriginalComponent customProp="value" someExtraValue={5} />
|
* // <OriginalComponent customProp="value" someExtraValue={5} />
|
||||||
*
|
*
|
||||||
* @returns A higher-order component with combined properties.
|
* @returns A higher-order component with combined properties.
|
||||||
|
*
|
||||||
|
* @param Component - The original component to wrap.
|
||||||
|
* @param sanitizeServerOnlyProps - If true, server-only props will be removed from the merged props. @default true if the component is not a server component, false otherwise.
|
||||||
|
* @param toMergeIntoProps - The properties to merge into the passed props.
|
||||||
*/
|
*/
|
||||||
export function withMergedProps<ToMergeIntoProps, CompleteReturnProps>({
|
export function withMergedProps<ToMergeIntoProps, CompleteReturnProps>({
|
||||||
Component,
|
Component,
|
||||||
sanitizeServerOnlyProps = true,
|
sanitizeServerOnlyProps,
|
||||||
toMergeIntoProps,
|
toMergeIntoProps,
|
||||||
}: {
|
}: {
|
||||||
Component: React.FC<CompleteReturnProps>
|
Component: React.FC<CompleteReturnProps>
|
||||||
sanitizeServerOnlyProps?: boolean
|
sanitizeServerOnlyProps?: boolean
|
||||||
toMergeIntoProps: ToMergeIntoProps
|
toMergeIntoProps: ToMergeIntoProps
|
||||||
}): React.FC<CompleteReturnProps> {
|
}): React.FC<CompleteReturnProps> {
|
||||||
|
if (sanitizeServerOnlyProps === undefined) {
|
||||||
|
sanitizeServerOnlyProps = !isReactServerComponentOrFunction(Component)
|
||||||
|
}
|
||||||
// A wrapper around the args.Component to inject the args.toMergeArgs as props, which are merged with the passed props
|
// A wrapper around the args.Component to inject the args.toMergeArgs as props, which are merged with the passed props
|
||||||
const MergedPropsComponent: React.FC<CompleteReturnProps> = (passedProps) => {
|
const MergedPropsComponent: React.FC<CompleteReturnProps> = (passedProps) => {
|
||||||
const mergedProps = deepMerge(passedProps, toMergeIntoProps)
|
const mergedProps = deepMerge(passedProps, toMergeIntoProps)
|
||||||
|
|
||||||
if (sanitizeServerOnlyProps) {
|
if (sanitizeServerOnlyProps) {
|
||||||
serverProps.forEach((prop) => {
|
serverProps.forEach((prop) => {
|
||||||
delete (mergedProps)[prop]
|
delete mergedProps[prop]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const mapFields = (args: {
|
|||||||
const fieldIsPresentational = fieldIsPresentationalOnly(field)
|
const fieldIsPresentational = fieldIsPresentationalOnly(field)
|
||||||
let CustomFieldComponent: CustomComponent<FieldComponentProps> = field.admin?.components?.Field
|
let CustomFieldComponent: CustomComponent<FieldComponentProps> = field.admin?.components?.Field
|
||||||
|
|
||||||
const CustomCellComponent = field.admin?.components?.Cell
|
let CustomCellComponent = field.admin?.components?.Cell
|
||||||
|
|
||||||
const isHidden = field?.admin && 'hidden' in field.admin && field.admin.hidden
|
const isHidden = field?.admin && 'hidden' in field.admin && field.admin.hidden
|
||||||
|
|
||||||
@@ -238,6 +238,7 @@ export const mapFields = (args: {
|
|||||||
labels: 'labels' in field ? field.labels : undefined,
|
labels: 'labels' in field ? field.labels : undefined,
|
||||||
options: 'options' in field ? fieldOptions : undefined,
|
options: 'options' in field ? fieldOptions : undefined,
|
||||||
relationTo: 'relationTo' in field ? field.relationTo : undefined,
|
relationTo: 'relationTo' in field ? field.relationTo : undefined,
|
||||||
|
schemaPath: path,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
@@ -588,9 +589,7 @@ export const mapFields = (args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (RichTextCellComponent) {
|
if (RichTextCellComponent) {
|
||||||
cellComponentProps.CellComponentOverride = (
|
CustomCellComponent = RichTextCellComponent
|
||||||
<WithServerSideProps Component={RichTextCellComponent} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldComponentProps = richTextField
|
fieldComponentProps = richTextField
|
||||||
@@ -788,6 +787,7 @@ export const mapFields = (args: {
|
|||||||
CustomField: null,
|
CustomField: null,
|
||||||
cellComponentProps: {
|
cellComponentProps: {
|
||||||
name: 'id',
|
name: 'id',
|
||||||
|
schemaPath: 'id',
|
||||||
},
|
},
|
||||||
disableBulkEdit: true,
|
disableBulkEdit: true,
|
||||||
fieldComponentProps: {
|
fieldComponentProps: {
|
||||||
|
|||||||
@@ -530,33 +530,52 @@ describe('lexicalBlocks', () => {
|
|||||||
await expect(paragraphInSubEditor).toBeVisible()
|
await expect(paragraphInSubEditor).toBeVisible()
|
||||||
await paragraphInSubEditor.click()
|
await paragraphInSubEditor.click()
|
||||||
await page.keyboard.type('Some subText')
|
await page.keyboard.type('Some subText')
|
||||||
|
|
||||||
// Upload something
|
// Upload something
|
||||||
const chooseExistingUploadButton = newSubLexicalAndUploadBlock
|
await expect(async () => {
|
||||||
.locator('.upload__toggler.list-drawer__toggler')
|
const chooseExistingUploadButton = newSubLexicalAndUploadBlock
|
||||||
.first()
|
.locator('.upload__toggler.list-drawer__toggler')
|
||||||
await expect(chooseExistingUploadButton).toBeVisible()
|
.first()
|
||||||
await chooseExistingUploadButton.click()
|
await wait(300)
|
||||||
await wait(500) // wait for drawer form state to initialize (it's a flake)
|
await expect(chooseExistingUploadButton).toBeVisible()
|
||||||
const uploadListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
|
await wait(300)
|
||||||
await expect(uploadListDrawer).toBeVisible()
|
await chooseExistingUploadButton.click()
|
||||||
// find button which has a span with text "payload.jpg" and click it in playwright
|
await wait(500) // wait for drawer form state to initialize (it's a flake)
|
||||||
const uploadButton = uploadListDrawer.locator('button').getByText('payload.jpg').first()
|
const uploadListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
|
||||||
await expect(uploadButton).toBeVisible()
|
await expect(uploadListDrawer).toBeVisible()
|
||||||
await uploadButton.click()
|
await wait(300)
|
||||||
await expect(uploadListDrawer).toBeHidden()
|
|
||||||
// Check if the upload is there
|
// find button which has a span with text "payload.jpg" and click it in playwright
|
||||||
await expect(
|
const uploadButton = uploadListDrawer.locator('button').getByText('payload.jpg').first()
|
||||||
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
|
await expect(uploadButton).toBeVisible()
|
||||||
).toHaveText('payload.jpg')
|
await wait(300)
|
||||||
|
await uploadButton.click()
|
||||||
|
await wait(300)
|
||||||
|
await expect(uploadListDrawer).toBeHidden()
|
||||||
|
// Check if the upload is there
|
||||||
|
await expect(
|
||||||
|
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
|
||||||
|
).toHaveText('payload.jpg')
|
||||||
|
}).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
})
|
||||||
|
|
||||||
|
await wait(300)
|
||||||
|
|
||||||
// save document and assert
|
// save document and assert
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
await wait(300)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
|
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
|
||||||
).toHaveText('payload.jpg')
|
).toHaveText('payload.jpg')
|
||||||
await expect(paragraphInSubEditor).toHaveText('Some subText')
|
await expect(paragraphInSubEditor).toHaveText('Some subText')
|
||||||
|
await wait(300)
|
||||||
|
|
||||||
// reload page and assert again
|
// reload page and assert again
|
||||||
await page.reload()
|
await page.reload()
|
||||||
|
await wait(300)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
|
newSubLexicalAndUploadBlock.locator('.field-type.upload .file-meta__url a'),
|
||||||
).toHaveText('payload.jpg')
|
).toHaveText('payload.jpg')
|
||||||
|
|||||||
Reference in New Issue
Block a user