fix: incorrect key property in Tabs field component (#4311)

Fixes #4282
This commit is contained in:
Alessio Gravili
2023-11-29 22:18:40 +01:00
committed by GitHub
parent b8fa61942e
commit 3502ce720b
11 changed files with 126 additions and 29 deletions

View File

@@ -1,12 +1,17 @@
import React from 'react'
import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../fields/config/types'
import type { RichTextAdapter } from './types'
const RichText: React.FC<RichTextField> = (props) => {
const RichText: React.FC<RichTextField> = (fieldprops) => {
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.editor
return <editor.FieldComponent {...props} />
const editor: RichTextAdapter = fieldprops.editor
const { FieldComponent } = editor
const FieldComponentImpl: React.FC<any> = useMemo(() => {
return FieldComponent()
}, [FieldComponent])
return <FieldComponentImpl {...fieldprops} />
}
export default RichText

View File

@@ -18,10 +18,10 @@ export type RichTextAdapter<
AdapterProps = any,
ExtraFieldProperties = {},
> = {
CellComponent: React.FC<
CellComponent: () => React.FC<
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
>
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
FieldComponent: () => React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
afterReadPromise?: ({
field,
incomingEditorState,

View File

@@ -95,7 +95,7 @@ const TabsField: React.FC<Props> = (props) => {
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
setActiveTabIndex(initialIndex || 0)
}
getInitialPref()
void getInitialPref()
}, [path, indexPath, getPreference, preferencesKey, tabsPrefKey])
const handleTabChange = useCallback(
@@ -193,7 +193,11 @@ const TabsField: React.FC<Props> = (props) => {
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
key={String(activeTabConfig.label)}
key={
activeTabConfig.label
? getTranslation(activeTabConfig.label, i18n)
: activeTabConfig['name']
}
margins="small"
permissions={
tabHasName(activeTabConfig) && permissions?.[activeTabConfig.name]

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import type { RichTextField } from '../../../../../../../../fields/config/types'
import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types'
@@ -7,8 +7,13 @@ import type { CellComponentProps } from '../../types'
const RichTextCell: React.FC<CellComponentProps<RichTextField>> = (props) => {
// eslint-disable-next-line react/destructuring-assignment
const editor: RichTextAdapter = props.field.editor
const { CellComponent } = editor
return <editor.CellComponent {...props} />
const CellComponentImpl: React.FC<any> = useMemo(() => {
return CellComponent()
}, [CellComponent])
return <CellComponentImpl {...props} />
}
export default RichTextCell

View File

@@ -64,6 +64,7 @@ const RichText: React.FC<FieldProps> = (props) => {
return (
<div
className={classes}
key={path}
style={{
...style,
width,
@@ -88,6 +89,7 @@ const RichText: React.FC<FieldProps> = (props) => {
setValue(serializedEditorState)
}}
path={path}
readOnly={readOnly}
value={value}
/>

View File

@@ -16,11 +16,12 @@ export type LexicalProviderProps = {
editorConfig: SanitizedEditorConfig
fieldProps: FieldProps
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
path: string
readOnly: boolean
value: SerializedEditorState
}
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
const { editorConfig, fieldProps, onChange, readOnly } = props
const { editorConfig, fieldProps, onChange, path, readOnly } = props
let { value } = props
if (editorConfig?.features?.hooks?.load?.length) {
@@ -60,7 +61,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
}
return (
<LexicalComposer initialConfig={initialConfig}>
<LexicalComposer initialConfig={initialConfig} key={path}>
<EditorConfigProvider editorConfig={editorConfig} fieldProps={fieldProps}>
<div className="editor-shell">
<LexicalEditorComponent editorConfig={editorConfig} onChange={onChange} />

View File

@@ -57,14 +57,16 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
}
return {
CellComponent: withMergedProps({
Component: RichTextCell,
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
}),
FieldComponent: withMergedProps({
Component: RichTextField,
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
}),
CellComponent: () =>
withMergedProps({
Component: RichTextCell,
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
}),
FieldComponent: () =>
withMergedProps({
Component: RichTextField,
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
}),
afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => {
return new Promise<void>((resolve, reject) => {
const promises: Promise<void>[] = []

View File

@@ -13,14 +13,16 @@ export function slateEditor(
args: AdapterArguments,
): RichTextAdapter<any[], AdapterArguments, AdapterArguments> {
return {
CellComponent: withMergedProps({
Component: RichTextCell,
toMergeIntoProps: args,
}),
FieldComponent: withMergedProps({
Component: RichTextField,
toMergeIntoProps: args,
}),
CellComponent: () =>
withMergedProps({
Component: RichTextCell,
toMergeIntoProps: args,
}),
FieldComponent: () =>
withMergedProps({
Component: RichTextField,
toMergeIntoProps: args,
}),
outputSchema: ({ isRequired }) => {
return {
items: {

View File

@@ -28,6 +28,7 @@ import TextFields from './collections/Text'
import Uploads from './collections/Upload'
import Uploads2 from './collections/Upload2'
import Uploads3 from './collections/Uploads3'
import TabsWithRichText from './globals/TabsWithRichText'
import { clearAndSeedEverything } from './seed'
export const collectionSlugs: CollectionConfig[] = [
@@ -85,6 +86,7 @@ export default buildConfigWithDefaults({
}),
},
collections: collectionSlugs,
globals: [TabsWithRichText],
localization: {
defaultLocale: 'en',
fallback: true,

View File

@@ -0,0 +1,53 @@
/**
* IMPORTANT: Do not change this style. This specific configuration is needed to reproduce this issue before it was fixed (https://github.com/payloadcms/payload/issues/4282):
* - lexicalEditor initialized on the outside and then shared between two richText fields
* - tabs field with two tabs, each with a richText field
* - each tab has a different label in each language. Needs to be a LOCALIZED label, not a single label for all languages. Only then can it be reproduced
*/
import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types'
import { lexicalEditor } from '../../../packages/richtext-lexical/src'
const initializedEditor = lexicalEditor()
const TabsWithRichText: GlobalConfig = {
slug: 'tabsWithRichText',
fields: [
{
type: 'tabs',
tabs: [
{
name: 'tab1',
label: {
en: 'en tab1',
es: 'es tab1',
},
fields: [
{
name: 'rt1',
type: 'richText',
editor: initializedEditor,
},
],
},
{
name: 'tab2',
label: {
en: 'en tab2',
es: 'es tab2',
},
fields: [
{
name: 'rt2',
type: 'richText',
editor: initializedEditor,
},
],
},
],
},
],
}
export default TabsWithRichText

View File

@@ -184,6 +184,27 @@ describe('lexical', () => {
expect(textNode2.format).toBe(0)
})
test('Make sure highly specific issue does not occur when two richText fields share the same editor prop', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4282
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'tabsWithRichText')
await page.goto(url.global('tabsWithRichText'))
const richTextField = page.locator('.rich-text-lexical').first()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
await richTextField.click() // Use click, because focus does not work
await page.keyboard.type('some text')
await page.locator('.tabs-field__tabs').first().getByText('en tab2').first().click()
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const contentEditable = richTextField.locator('.ContentEditable__root').first()
const textContent = await contentEditable.textContent()
expect(textContent).not.toBe('some text')
expect(textContent).toBe('')
})
describe('nested lexical editor in block', () => {
test('should type and save typed text', async () => {
await navigateToLexicalFields()