feat: pass path to FieldDescription (#4364)
fix: DescriptionFunction type Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This commit is contained in:
@@ -251,11 +251,16 @@ const field = {
|
||||
|
||||
### Description
|
||||
|
||||
A description can be configured three ways.
|
||||
A description can be configured in three ways.
|
||||
|
||||
- As a string
|
||||
- As a function that accepts an object containing the field's value, which returns a string
|
||||
- As a React component that accepts value as a prop
|
||||
- As a function which returns a string
|
||||
- As a React component
|
||||
|
||||
Functions are called with an optional argument object with the following shape, and React components are rendered with the following props:
|
||||
|
||||
- `path` - the path of the field
|
||||
- `value` - the current value of the field
|
||||
|
||||
As shown above, you can simply provide a string that will show by the field, but there are use cases where you may want to create some dynamic feedback. By using a function or a component for the `description` property you can provide realtime feedback as the user interacts with the form.
|
||||
|
||||
@@ -269,8 +274,8 @@ As shown above, you can simply provide a string that will show by the field, but
|
||||
type: 'text',
|
||||
maxLength: 20,
|
||||
admin: {
|
||||
description: ({ value }) =>
|
||||
`${typeof value === 'string' ? 20 - value.length : '20'} characters left`,
|
||||
description: ({ path, value }) =>
|
||||
`${typeof value === 'string' ? 20 - value.length : '20'} characters left (field: ${path})`,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -290,11 +295,12 @@ This example will display the number of characters allowed as the user types.
|
||||
maxLength: 20,
|
||||
admin: {
|
||||
description:
|
||||
({ value }) => (
|
||||
({ path, value }) => (
|
||||
<div>
|
||||
Character count:
|
||||
{' '}
|
||||
{ value?.length || 0 }
|
||||
(field: {path})
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -303,7 +309,7 @@ This example will display the number of characters allowed as the user types.
|
||||
}
|
||||
```
|
||||
|
||||
This component will count the number of characters entered.
|
||||
This component will count the number of characters entered, as well as display the path of the field.
|
||||
|
||||
### TypeScript
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import { isComponent } from './types'
|
||||
const baseClass = 'field-description'
|
||||
|
||||
const FieldDescription: React.FC<Props> = (props) => {
|
||||
const { className, description, value, marginPlacement } = props
|
||||
const { className, description, marginPlacement, path, value } = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
if (isComponent(description)) {
|
||||
const Description = description
|
||||
return <Description value={value} />
|
||||
return <Description path={path} value={value} />
|
||||
}
|
||||
|
||||
if (description) {
|
||||
@@ -31,7 +31,7 @@ const FieldDescription: React.FC<Props> = (props) => {
|
||||
.join(' ')}
|
||||
>
|
||||
{typeof description === 'function'
|
||||
? description({ value })
|
||||
? description({ path, value })
|
||||
: getTranslation(description, i18n)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
export type DescriptionFunction = (value?: unknown) => string
|
||||
type Args<T = unknown> = {
|
||||
path: string
|
||||
value?: T
|
||||
}
|
||||
export type DescriptionFunction<T = unknown> = (args: Args<T>) => string
|
||||
|
||||
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
|
||||
export type DescriptionComponent<T = unknown> = React.ComponentType<Args<T>>
|
||||
|
||||
export type Description =
|
||||
| DescriptionComponent
|
||||
@@ -13,8 +17,9 @@ export type Description =
|
||||
export type Props = {
|
||||
className?: string
|
||||
description?: Description
|
||||
marginPlacement?: 'bottom' | 'top'
|
||||
path?: string
|
||||
value?: unknown
|
||||
marginPlacement?: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
export function isComponent(description: Description): description is DescriptionComponent {
|
||||
|
||||
@@ -214,6 +214,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
path={path}
|
||||
value={value}
|
||||
/>
|
||||
</header>
|
||||
|
||||
@@ -215,7 +215,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</header>
|
||||
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
|
||||
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (
|
||||
|
||||
@@ -96,7 +96,7 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ const Code: React.FC<Props> = (props) => {
|
||||
readOnly={readOnly}
|
||||
value={(value as string) || ''}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import RenderFields from '../../RenderFields'
|
||||
import { RowLabel } from '../../RowLabel'
|
||||
import { WatchChildErrors } from '../../WatchChildErrors'
|
||||
import withCondition from '../../withCondition'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'collapsible-field'
|
||||
|
||||
@@ -89,7 +89,6 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
|
||||
className={[
|
||||
fieldBaseClass,
|
||||
baseClass,
|
||||
@@ -98,6 +97,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
|
||||
>
|
||||
<WatchChildErrors fieldSchema={fields} path={path} setErrorCount={setErrorCount} />
|
||||
<Collapsible
|
||||
@@ -125,7 +125,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</Collapsible>
|
||||
<FieldDescription description={description} />
|
||||
<FieldDescription description={description} path={path} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
/>
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ const Group: React.FC<Props> = (props) => {
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
path={path}
|
||||
value={null}
|
||||
/>
|
||||
</header>
|
||||
|
||||
@@ -97,7 +97,7 @@ const JSONField: React.FC<Props> = (props) => {
|
||||
readOnly={readOnly}
|
||||
value={stringValue}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ import { optionIsObject } from '../../../../../fields/config/types'
|
||||
import DefaultError from '../../Error'
|
||||
import FieldDescription from '../../FieldDescription'
|
||||
import DefaultLabel from '../../Label'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
import RadioInput from './RadioInput'
|
||||
import './index.scss'
|
||||
import { fieldBaseClass } from '../shared'
|
||||
|
||||
const baseClass = 'radio-group'
|
||||
|
||||
export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
className?: string
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -28,13 +30,13 @@ export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
}
|
||||
|
||||
const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
const {
|
||||
name,
|
||||
Error,
|
||||
Label,
|
||||
className,
|
||||
description,
|
||||
errorMessage,
|
||||
@@ -49,8 +51,6 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
} = props
|
||||
|
||||
const ErrorComp = Error || DefaultError
|
||||
@@ -103,7 +103,7 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ const RadioGroup: React.FC<Props> = (props) => {
|
||||
name,
|
||||
admin: {
|
||||
className,
|
||||
components: { Error, Label } = {},
|
||||
condition,
|
||||
description,
|
||||
layout = 'horizontal',
|
||||
readOnly,
|
||||
style,
|
||||
width,
|
||||
components: { Error, Label } = {},
|
||||
} = {},
|
||||
label,
|
||||
options,
|
||||
@@ -44,6 +44,8 @@ const RadioGroup: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<RadioGroupInput
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
className={className}
|
||||
description={description}
|
||||
errorMessage={errorMessage}
|
||||
@@ -58,8 +60,6 @@ const RadioGroup: React.FC<Props> = (props) => {
|
||||
style={style}
|
||||
value={value}
|
||||
width={width}
|
||||
Error={Error}
|
||||
Label={Label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
{errorLoading && <div className={`${baseClass}__error-loading`}>{errorLoading}</div>}
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { fieldBaseClass } from '../shared'
|
||||
import './index.scss'
|
||||
|
||||
export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
className?: string
|
||||
description?: Description
|
||||
errorMessage?: string
|
||||
@@ -29,12 +31,12 @@ export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> &
|
||||
style?: React.CSSProperties
|
||||
value?: string | string[]
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
}
|
||||
|
||||
const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
className,
|
||||
defaultValue,
|
||||
description,
|
||||
@@ -52,8 +54,6 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
} = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -111,7 +111,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
showError={showError}
|
||||
value={valueToRender as Option}
|
||||
/>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,6 +177,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__description`}
|
||||
description={activeTabConfig.description}
|
||||
marginPlacement="bottom"
|
||||
path={path}
|
||||
/>
|
||||
<RenderFields
|
||||
fieldSchema={activeTabConfig.fields.map((field) => {
|
||||
|
||||
@@ -95,6 +95,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
||||
description={description}
|
||||
path={path}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
||||
</div>
|
||||
</label>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import './index.scss'
|
||||
const baseClass = 'upload'
|
||||
|
||||
export type UploadInputProps = Omit<UploadField, 'type'> & {
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
api?: string
|
||||
className?: string
|
||||
collection?: SanitizedCollectionConfig
|
||||
@@ -41,12 +43,12 @@ export type UploadInputProps = Omit<UploadField, 'type'> & {
|
||||
style?: React.CSSProperties
|
||||
value?: string
|
||||
width?: string
|
||||
Error?: React.ComponentType<any>
|
||||
Label?: React.ComponentType<any>
|
||||
}
|
||||
|
||||
const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
const {
|
||||
Error,
|
||||
Label,
|
||||
api = '/api',
|
||||
className,
|
||||
collection,
|
||||
@@ -64,8 +66,6 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
style,
|
||||
value,
|
||||
width,
|
||||
Error,
|
||||
Label,
|
||||
} = props
|
||||
|
||||
const { i18n, t } = useTranslation('fields')
|
||||
@@ -191,7 +191,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FieldDescription description={description} value={file} />
|
||||
<FieldDescription description={description} path={path} value={file} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!readOnly && <DocumentDrawer onSave={onSave} />}
|
||||
|
||||
@@ -22,3 +22,9 @@ export {
|
||||
formatListDrawerSlug,
|
||||
useListDrawer,
|
||||
} from '../../admin/components/elements/ListDrawer'
|
||||
|
||||
export {
|
||||
Description,
|
||||
DescriptionComponent,
|
||||
DescriptionFunction,
|
||||
} from '../../admin/components/forms/FieldDescription/types'
|
||||
|
||||
@@ -94,7 +94,7 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
value={value}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -445,7 +445,7 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Slate>
|
||||
<FieldDescription description={description} value={value} />
|
||||
<FieldDescription description={description} path={path} value={value} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections
|
||||
import { slateEditor } from '../../../packages/richtext-slate/src'
|
||||
import DemoUIFieldCell from '../components/DemoUIField/Cell'
|
||||
import DemoUIFieldField from '../components/DemoUIField/Field'
|
||||
import { FieldDescriptionComponent, FieldDescriptionFunction } from '../components/FieldDescription'
|
||||
import { slugPluralLabel, slugSingularLabel } from '../shared'
|
||||
import { postsCollectionSlug } from '../slugs'
|
||||
|
||||
@@ -93,5 +94,26 @@ export const Posts: CollectionConfig = {
|
||||
'This is a very long description that takes many characters to complete and hopefully will wrap instead of push the sidebar open, lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum voluptates. Quisquam, voluptatum voluptates.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'descriptionAsString',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Static field description.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'descriptionAsFunction',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: FieldDescriptionFunction,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'descriptionAsComponent',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: FieldDescriptionComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
18
test/admin/components/FieldDescription/index.tsx
Normal file
18
test/admin/components/FieldDescription/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import type {
|
||||
DescriptionComponent,
|
||||
DescriptionFunction,
|
||||
} from '../../../../packages/payload/src/admin/components/forms/FieldDescription/types'
|
||||
|
||||
export const FieldDescriptionComponent: DescriptionComponent<string> = ({ path, value }) => {
|
||||
return (
|
||||
<div>
|
||||
Component description: {path} - {value}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FieldDescriptionFunction: DescriptionFunction<string> = ({ path, value }) => {
|
||||
return `Function description: ${path} - ${value}`
|
||||
}
|
||||
@@ -1157,6 +1157,29 @@ describe('admin', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Field descriptions', () => {
|
||||
test('should render static field description', async () => {
|
||||
await page.goto(url.create)
|
||||
await expect(page.locator('.field-description-descriptionAsString')).toContainText(
|
||||
'Static field description.',
|
||||
)
|
||||
})
|
||||
test('should render functional field description', async () => {
|
||||
await page.goto(url.create)
|
||||
await page.locator('#field-descriptionAsFunction').fill('functional')
|
||||
await expect(page.locator('.field-description-descriptionAsFunction')).toContainText(
|
||||
'Function description: descriptionAsFunction - functional',
|
||||
)
|
||||
})
|
||||
test('should render component field description', async () => {
|
||||
await page.goto(url.create)
|
||||
await page.locator('#field-descriptionAsComponent').fill('component')
|
||||
await expect(page.locator('.field-description-descriptionAsComponent')).toContainText(
|
||||
'Component description: descriptionAsComponent - component',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||
|
||||
Reference in New Issue
Block a user