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:
@@ -4,6 +4,7 @@ export type LabelProps = {
|
||||
htmlFor?: string
|
||||
label?: Record<string, string> | string
|
||||
required?: boolean
|
||||
schemaPath?: string
|
||||
unstyled?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {},
|
||||
})
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -144,6 +144,7 @@ export const mapFields = (args: {
|
||||
const labelProps: LabelProps = {
|
||||
label,
|
||||
required: 'required' in field ? field.required : undefined,
|
||||
schemaPath: path,
|
||||
}
|
||||
|
||||
const CustomLabelComponent =
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user