fix(ui): properly extracts label from field in FieldLabel component (#8190)

Although the `<FieldLabel />` component receives a `field` prop, it does
not use this prop to extract the `label` from the field. This is
currently only an issue when rendering this component directly, such as
within `admin.components.Label`. The label simply won't appear unless
explicitly provided, despite it being passed as `field.label`. This is
not an issue when rendering field components themselves, because they
properly thread this value through as a top-level prop.

Here's an example of the issue:

```tsx
import type { TextFieldLabelServerComponent } from 'payload'

import { FieldLabel } from '@payloadcms/ui'
import React from 'react'

export const MyCustomLabelComponent: TextFieldLabelServerComponent = ({ clientField }) => {
  return (
    <FieldLabel
      field={clientField}
      label={clientField.label} // this should not be needed!
    />
  )
}
```

Here is the end result:

```tsx
import type { TextFieldLabelServerComponent } from 'payload'

import { FieldLabel } from '@payloadcms/ui'
import React from 'react'

export const MyCustomLabelComponent: TextFieldLabelServerComponent = ({ clientField }) => {
  return <FieldLabel field={clientField} />
}
```
This commit is contained in:
Jacob Fletcher
2024-09-12 14:45:17 -04:00
committed by GitHub
parent 532e4b52fe
commit a6f13f7330
24 changed files with 39 additions and 96 deletions

View File

@@ -1,5 +1,5 @@
import type { ServerProps, StaticLabel } from '../../config/types.js'
import type { ClientField, Field } from '../../fields/config/types.js'
import type { Field } from '../../fields/config/types.js'
import type { MappedComponent } from '../types.js'
import type { ClientFieldWithOptionalType } from './Field.js'
@@ -27,7 +27,7 @@ export type FieldLabelServerProps<
} & GenericLabelProps &
Partial<ServerProps>
export type SanitizedLabelProps<TFieldClient extends ClientField> = Omit<
export type SanitizedLabelProps<TFieldClient extends ClientFieldWithOptionalType> = Omit<
FieldLabelClientProps<TFieldClient>,
'label' | 'required'
>

View File

@@ -41,7 +41,6 @@ const RichTextComponent: React.FC<
style,
width,
} = {},
label,
required,
},
field,
@@ -101,13 +100,7 @@ const RichTextComponent: React.FC<
{...(errorProps || {})}
alignCaret="left"
/>
<FieldLabel
field={field}
Label={Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={Label} {...(labelProps || {})} />
<div className={`${baseClass}__wrap`}>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider

View File

@@ -66,7 +66,6 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
style,
width,
} = {},
label,
required,
},
labelProps,
@@ -321,13 +320,7 @@ const RichTextField: React.FC<LoadedSlateFieldProps> = (props) => {
width,
}}
>
<FieldLabel
Label={Label}
label={label}
required={required}
{...(labelProps || {})}
field={field}
/>
<FieldLabel Label={Label} {...(labelProps || {})} field={field} />
<div className={`${baseClass}__wrap`}>
<FieldError CustomError={Error} field={field} path={path} {...(errorProps || {})} />
<Slate

View File

@@ -101,14 +101,7 @@ export const buildColumnState = (args: Args): Column[] => {
? field.admin.components.Label
: undefined
const Label = (
<FieldLabel
field={field}
Label={CustomLabelToRender}
label={'label' in field ? (field.label as StaticLabel) : undefined}
unstyled
/>
)
const Label = <FieldLabel field={field} Label={CustomLabelToRender} unstyled />
const fieldAffectsDataSubFields =
field &&

View File

@@ -239,8 +239,6 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
as="span"
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
unstyled
{...(labelProps || {})}
/>

View File

@@ -231,8 +231,6 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
as="span"
field={field}
Label={field?.admin?.components?.Description}
label={label}
required={required}
unstyled
{...(labelProps || {})}
/>

View File

@@ -83,13 +83,7 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => {
width,
}}
>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError
CustomError={field?.admin?.components?.Error}

View File

@@ -81,13 +81,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
width,
}}
>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`} id={`field-${path.replace(/\./g, '__')}`}>
<FieldError
CustomError={field?.admin?.components?.Error}

View File

@@ -77,13 +77,7 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => {
width,
}}
>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError
CustomError={field?.admin?.components?.Error}

View File

@@ -1,6 +1,6 @@
'use client'
import type { FieldLabelClientComponent, GenericLabelProps } from 'payload'
import type { FieldLabelClientComponent, GenericLabelProps, StaticLabel } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
@@ -44,9 +44,25 @@ const DefaultFieldLabel: React.FC<GenericLabelProps> = (props) => {
export const FieldLabel: FieldLabelClientComponent = (props) => {
const { Label, ...rest } = props
// Don't get `Label` from `field.admin.components.Label` here because
// this will cause an infinite loop when threading field through custom usages of `FieldLabel`
if (Label) {
return <RenderComponent clientProps={rest} mappedComponent={Label} />
}
return <DefaultFieldLabel {...rest} />
return (
<DefaultFieldLabel
{...rest}
label={
typeof props?.label !== 'undefined'
? props.label
: props?.field && 'label' in props.field && (props.field.label as StaticLabel) // type assertion needed for `row` fields
}
required={
typeof props.required !== 'undefined'
? props.required
: props?.field && 'required' in props.field && (props.field?.required as boolean) // type assertion needed for `group` fields
}
/>
)
}

View File

@@ -133,13 +133,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => {
width,
}}
>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError
CustomError={field?.admin?.components?.Error}

View File

@@ -147,13 +147,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
width,
}}
>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError
CustomError={field?.admin?.components?.Error}

View File

@@ -110,6 +110,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => {
<ul className={`${baseClass}__wrap`}>
<li>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
{...getCoordinateFieldLabel('longitude')}
/>

View File

@@ -99,13 +99,7 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => {
{...(errorProps || {})}
alignCaret="left"
/>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {

View File

@@ -55,7 +55,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
width,
} = {},
hasMany,
label,
relationTo,
required,
},
@@ -602,13 +601,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
width,
}}
>
<FieldLabel
field={field}
Label={field?.admin?.components?.Label}
label={label}
required={required}
{...(labelProps || {})}
/>
<FieldLabel field={field} Label={field?.admin?.components?.Label} {...(labelProps || {})} />
<div className={`${fieldBaseClass}__wrap`}>
<FieldError
CustomError={field?.admin?.components?.Error}

View File

@@ -36,7 +36,7 @@ export type SelectInputProps = {
readonly isClearable?: boolean
readonly isSortable?: boolean
readonly Label?: MappedComponent
readonly label: StaticLabel
readonly label?: StaticLabel
readonly labelProps?: Record<string, unknown>
readonly name: string
readonly onChange?: ReactSelectAdapterProps['onChange']

View File

@@ -44,7 +44,6 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
width,
} = {} as SelectFieldClientProps['field']['admin'],
hasMany = false,
label,
options: optionsFromProps = [],
required,
},
@@ -111,7 +110,6 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
isClearable={isClearable}
isSortable={isSortable}
Label={field?.admin?.components?.Label}
label={label}
name={name}
onChange={onChange}
options={options}

View File

@@ -33,7 +33,6 @@ const TextFieldComponent: TextFieldClientComponent = (props) => {
width,
} = {},
hasMany,
label,
localized,
maxLength,
maxRows,
@@ -131,7 +130,6 @@ const TextFieldComponent: TextFieldClientComponent = (props) => {
hasMany={hasMany}
inputRef={inputRef}
Label={field?.admin?.components?.Label}
label={label}
maxRows={maxRows}
minRows={minRows}
onChange={

View File

@@ -27,7 +27,7 @@ export type TextInputProps = {
readonly field?: MarkOptional<TextFieldClient, 'type'>
readonly inputRef?: React.RefObject<HTMLInputElement>
readonly Label?: MappedComponent
readonly label: StaticLabel
readonly label?: StaticLabel
readonly labelProps?: Record<string, unknown>
readonly maxRows?: number
readonly minRows?: number

View File

@@ -35,7 +35,6 @@ const TextareaFieldComponent: TextareaFieldClientComponent = (props) => {
style,
width,
} = {},
label,
localized,
maxLength,
minLength,
@@ -91,7 +90,6 @@ const TextareaFieldComponent: TextareaFieldClientComponent = (props) => {
Error={field?.admin?.components?.Error}
errorProps={errorProps}
Label={field?.admin?.components?.Label}
label={label}
labelProps={labelProps}
onChange={(e) => {
setValue(e.target.value)

View File

@@ -24,7 +24,7 @@ export type TextAreaInputProps = {
readonly field?: MarkOptional<TextareaFieldClient, 'type'>
readonly inputRef?: React.RefObject<HTMLInputElement>
readonly Label?: MappedComponent
readonly label: StaticLabel
readonly label?: StaticLabel
readonly labelProps?: FieldLabelClientProps<MarkOptional<TextareaFieldClient, 'type'>>
readonly onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>

View File

@@ -63,7 +63,7 @@ export type UploadInputProps = {
readonly hasMany?: boolean
readonly isSortable?: boolean
readonly Label?: MappedComponent
readonly label: StaticLabel
readonly label?: StaticLabel
readonly labelProps?: FieldLabelClientProps<MarkOptional<UploadFieldClient, 'type'>>
readonly maxRows?: number
readonly onChange?: (e) => void

View File

@@ -21,7 +21,6 @@ export function UploadComponent(props: UploadFieldClientProps) {
_path,
admin: { className, isSortable, readOnly: readOnlyFromAdmin, style, width } = {},
hasMany,
label,
maxRows,
relationTo,
required,
@@ -70,7 +69,6 @@ export function UploadComponent(props: UploadFieldClientProps) {
hasMany={hasMany}
isSortable={isSortable}
Label={field?.admin?.components?.Label}
label={label}
maxRows={maxRows}
onChange={setValue}
path={path}

View File

@@ -1,12 +1,14 @@
'use client'
import type { EmailFieldClientComponent } from 'payload'
import { useFieldProps } from '@payloadcms/ui'
import React from 'react'
export const CustomLabel = ({ schemaPath }) => {
export const CustomLabel: EmailFieldClientComponent = ({ field }) => {
const { path: pathFromContext } = useFieldProps()
const path = pathFromContext ?? schemaPath // pathFromContext will be undefined in list view
const path = pathFromContext ?? field?._schemaPath // pathFromContext will be undefined in list view
return (
<label className="custom-label" htmlFor={`field-${path.replace(/\./g, '__')}`}>