fix(ui,richtext-*): path from context should always have precedence over path from props, even if it's an empty string (#6782)

This commit is contained in:
Alessio Gravili
2024-06-15 01:42:16 -04:00
committed by GitHub
parent 628749573e
commit e7159c033e
29 changed files with 163 additions and 44 deletions

View File

@@ -4,6 +4,7 @@ export type LabelProps = {
htmlFor?: string
label?: Record<string, string> | string
required?: boolean
schemaPath?: string
unstyled?: boolean
}

View File

@@ -59,7 +59,7 @@ const _RichText: React.FC<
const { path: pathFromContext } = useFieldProps()
const fieldType = useField<SerializedEditorState>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -106,7 +106,7 @@ const RichTextField: React.FC<
const { formInitializing, initialValue, path, schemaPath, setValue, showError, value } = useField(
{
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
},
)

View File

@@ -125,7 +125,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
value,
} = useField<number>({
hasRows: true,
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -128,7 +128,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
value,
} = useField<number>({
hasRows: true,
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -65,7 +65,7 @@ const CheckboxField: React.FC<CheckboxFieldProps> = (props) => {
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
disableFormData,
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -66,7 +66,7 @@ const CodeField: React.FC<CodeFieldProps> = (props) => {
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -57,7 +57,7 @@ const CollapsibleField: React.FC<CollapsibleFieldProps> = (props) => {
const formInitializing = useFormInitializing()
const formProcessing = useFormProcessing()
const path = pathFromContext || pathFromProps
const path = pathFromContext ?? pathFromProps
const { i18n } = useTranslation()
const { getPreference, setPreference } = usePreferences()

View File

@@ -1,6 +1,6 @@
/* eslint-disable react/destructuring-assignment */
'use client'
import type { ClientValidate } from 'payload/types'
import type { ClientValidate, DateField } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback } from 'react'
@@ -14,8 +14,6 @@ import './index.scss'
const baseClass = 'date-time-field'
import type { DateField } from 'payload/types'
import type { FormFieldBase } from '../shared/index.js'
import { FieldDescription } from '../../forms/FieldDescription/index.js'
@@ -68,7 +66,7 @@ const DateTimeField: React.FC<DateFieldProps> = (props) => {
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
const { formInitializing, formProcessing, path, setValue, showError, value } = useField<Date>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -1,6 +1,5 @@
'use client'
import type { ClientValidate } from 'payload/types'
import type { EmailField as EmailFieldType } from 'payload/types'
import type { ClientValidate, EmailField as EmailFieldType } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback } from 'react'
@@ -62,7 +61,7 @@ const EmailField: React.FC<EmailFieldProps> = (props) => {
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -1,5 +1,5 @@
'use client'
import type { ClientValidate } from 'payload/types'
import type { ClientValidate, JSONField as JSONFieldType } from 'payload/types'
import React, { useCallback, useEffect, useState } from 'react'
@@ -12,8 +12,6 @@ import './index.scss'
const baseClass = 'json-field'
import type { JSONField as JSONFieldType } from 'payload/types'
import type { FormFieldBase } from '../shared/index.js'
import { FieldDescription } from '../../forms/FieldDescription/index.js'
@@ -67,7 +65,7 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
const { formInitializing, formProcessing, initialValue, path, setValue, showError, value } =
useField<string>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -77,7 +77,7 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
const { formInitializing, formProcessing, path, setValue, showError, value } = useField<
number | number[]
>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -71,7 +71,7 @@ const PointField: React.FC<PointFieldProps> = (props) => {
showError,
value = [null, null],
} = useField<[number, number]>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -77,7 +77,7 @@ const RadioGroupField: React.FC<RadioFieldProps> = (props) => {
showError,
value: valueFromContext,
} = useField<string>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -102,7 +102,7 @@ const RelationshipField: React.FC<RelationshipFieldProps> = (props) => {
showError,
value,
} = useField<Value | Value[]>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -76,7 +76,7 @@ const SelectField: React.FC<SelectFieldProps> = (props) => {
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -56,7 +56,7 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
} = useFieldProps()
const readOnly = readOnlyFromProps || readOnlyFromContext
const path = pathFromContext || pathFromProps || name
const path = pathFromContext ?? pathFromProps ?? name
const { getPreference, setPreference } = usePreferences()
const { preferencesKey } = useDocumentInfo()
const { i18n } = useTranslation()

View File

@@ -62,7 +62,7 @@ const TextField: React.FC<TextFieldProps> = (props) => {
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -68,7 +68,7 @@ const TextareaField: React.FC<TextareaFieldProps> = (props) => {
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
const { formInitializing, formProcessing, path, setValue, showError, value } = useField<string>({
path: pathFromContext || pathFromProps || name,
path: pathFromContext ?? pathFromProps ?? name,
validate: memoizedValidate,
})

View File

@@ -55,7 +55,7 @@ const _Upload: React.FC<UploadFieldProps> = (props) => {
const { filterOptions, formInitializing, formProcessing, path, setValue, showError, value } =
useField<string>({
path: pathFromContext || pathFromProps,
path: pathFromContext ?? pathFromProps,
validate: memoizedValidate,
})

View File

@@ -20,7 +20,7 @@ const DefaultFieldError: React.FC<ErrorProps> = (props) => {
} = props
const { path: pathFromContext } = useFieldProps()
const path = pathFromContext || pathFromProps
const path = pathFromContext ?? pathFromProps
const hasSubmitted = useFormSubmitted()
const field = useFormFields(([fields]) => (fields && fields?.[path]) || null)

View File

@@ -18,13 +18,13 @@ export type FieldPropsContextType = {
}
const FieldPropsContext = React.createContext<FieldPropsContextType>({
type: '' as keyof FieldTypes,
type: undefined as keyof FieldTypes,
custom: {},
indexPath: '',
path: '',
indexPath: undefined,
path: undefined,
permissions: {} as FieldPermissions,
readOnly: false,
schemaPath: '',
schemaPath: undefined,
siblingPermissions: {},
})

View File

@@ -10,7 +10,7 @@ export const withCondition = <P extends Record<string, unknown>>(
const CheckForCondition: React.FC<P> = (props) => {
const { name } = props
const { type, indexPath, path: pathFromContext } = useFieldProps()
const path = pathFromContext || name
const path = pathFromContext ?? name
return (
<WatchCondition indexPath={indexPath} name={name as string} path={path as string} type={type}>

View File

@@ -144,6 +144,7 @@ export const mapFields = (args: {
const labelProps: LabelProps = {
label,
required: 'required' in field ? field.required : undefined,
schemaPath: path,
}
const CustomLabelComponent =

View File

@@ -1,7 +1,6 @@
import type { ArrayField, Block } from 'payload/types'
import { BlocksFeature, FixedToolbarFeature } from '@payloadcms/richtext-lexical'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { BlocksFeature, FixedToolbarFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { textFieldsSlug } from '../Text/shared.js'
@@ -251,3 +250,34 @@ export const SubBlockBlock: Block = {
},
],
}
export const TabBlock: Block = {
slug: 'tabBlock',
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Tab1',
name: 'tab1',
fields: [
{
name: 'text1',
type: 'text',
},
],
},
{
label: 'Tab2',
name: 'tab2',
fields: [
{
name: 'text2',
type: 'text',
},
],
},
],
},
],
}

View File

@@ -500,7 +500,7 @@ describe('lexicalBlocks', () => {
const newRichTextBlock = richTextField
.locator('.lexical-block:not(.lexical-block .lexical-block)')
.last() // The :not(.lexical-block .lexical-block) makes sure this does not select sub-blocks
.nth(8) // The :not(.lexical-block .lexical-block) makes sure this does not select sub-blocks
await newRichTextBlock.scrollIntoViewIfNeeded()
await expect(newRichTextBlock).toBeVisible()
@@ -888,13 +888,9 @@ describe('lexicalBlocks', () => {
test('should respect required error state in deeply nested text field', async () => {
await navigateToLexicalFields()
await wait(300)
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await wait(300)
await richTextField.scrollIntoViewIfNeeded()
await wait(300)
await expect(richTextField).toBeVisible()
await wait(300)
@@ -937,5 +933,80 @@ describe('lexicalBlocks', () => {
await expect(requiredTooltip).toBeInViewport() // toBeVisible() doesn't work for some reason
})
// Reproduces https://github.com/payloadcms/payload/issues/6631
test('ensure tabs field within lexical block correctly loads and saves data', async () => {
await navigateToLexicalFields()
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
const tabsBlock = richTextField.locator('.lexical-block').nth(8)
await wait(300)
await tabsBlock.scrollIntoViewIfNeeded()
await wait(300)
await expect(tabsBlock).toBeVisible()
await wait(300)
const tab1Text1Field = tabsBlock.locator('#field-tab1__text1')
await tab1Text1Field.scrollIntoViewIfNeeded()
await expect(tab1Text1Field).toBeVisible()
await expect(tab1Text1Field).toHaveValue('Some text1')
// change text to 'Some text1 changed'
await tab1Text1Field.fill('Some text1 changed')
const tab1Button = tabsBlock.locator('.tabs-field__tab-button', { hasText: 'Tab1' })
const tab2Button = tabsBlock.locator('.tabs-field__tab-button', { hasText: 'Tab2' })
await tab2Button.click()
await wait(300)
const tab2Text1Field = tabsBlock.locator('#field-tab2__text2')
await tab2Text1Field.scrollIntoViewIfNeeded()
await expect(tab2Text1Field).toBeVisible()
await expect(tab2Text1Field).toHaveValue('Some text2')
// change text to 'Some text2 changed'
await tab2Text1Field.fill('Some text2 changed')
await wait(300)
await saveDocAndAssert(page)
await wait(300)
await tabsBlock.scrollIntoViewIfNeeded()
await wait(300)
await expect(tabsBlock).toBeVisible()
await wait(300)
await tab1Button.click()
await expect(tab1Text1Field).toHaveValue('Some text1 changed')
await tab2Button.click()
await expect(tab2Text1Field).toHaveValue('Some text2 changed')
await expect(async () => {
const lexicalDoc: LexicalField = (
await payload.find({
collection: lexicalFieldsSlug,
depth: 0,
overrideAccess: true,
where: {
title: {
equals: lexicalDocData.title,
},
},
})
).docs[0] as never
const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks
const tabBlockData: SerializedBlockNode = lexicalField.root
.children[13] as SerializedBlockNode
expect(tabBlockData.fields.tab1.text1).toBe('Some text1 changed')
expect(tabBlockData.fields.tab2.text2).toBe('Some text2 changed')
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
})
})

View File

@@ -273,6 +273,22 @@ export function generateLexicalRichText() {
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '666c9dfd189d72626ea301f9',
blockName: '',
tab1: {
text1: 'Some text1',
},
tab2: {
text2: 'Some text2',
},
blockType: 'tabBlock',
},
},
],
direction: 'ltr',
},

View File

@@ -4,8 +4,6 @@ import type { CollectionConfig } from 'payload/types'
import { createHeadlessEditor } from '@lexical/headless'
import { $convertToMarkdownString } from '@lexical/markdown'
import { getEnabledNodes } from '@payloadcms/richtext-lexical'
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
import {
BlocksFeature,
FixedToolbarFeature,
@@ -14,7 +12,9 @@ import {
TreeViewFeature,
UploadFeature,
defaultEditorFeatures,
getEnabledNodes,
lexicalEditor,
sanitizeServerEditorConfig,
} from '@payloadcms/richtext-lexical'
import { lexicalFieldsSlug } from '../../slugs.js'
@@ -26,6 +26,7 @@ import {
RichTextBlock,
SelectFieldBlock,
SubBlockBlock,
TabBlock,
TextBlock,
UploadAndRichTextBlock,
} from './blocks.js'
@@ -77,6 +78,7 @@ const editorConfig: ServerEditorConfig = {
SubBlockBlock,
RadioButtonsBlock,
ConditionalLayoutBlock,
TabBlock,
],
}),
],

View File

@@ -3,8 +3,11 @@
import { useFieldProps } from '@payloadcms/ui/forms/FieldPropsProvider'
import React from 'react'
const CustomLabel = () => {
const { path } = useFieldProps()
const CustomLabel = ({ schemaPath }) => {
const { path: pathFromContext } = useFieldProps()
const path = pathFromContext ?? schemaPath // pathFromContext will be undefined in list view
return (
<label className="custom-label" htmlFor={`field-${path.replace(/\./g, '__')}`}>
#label