Files
payload/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx
Jacob Fletcher eb037a0cc6 fix: passes field permissions to custom fields (#10024)
Fixes #9888. Field permissions were not being passed into custom
components. This led to custom components, such as arrays and blocks,
unable to render default Payload fields. This was because their props
lacked the permissions object required for rendering. For example:

```ts
'use client'
import type { ArrayFieldClientComponent } from 'payload'
import { ArrayField } from '@payloadcms/ui'

export const MyArray: ArrayFieldClientComponent = (props) => <ArrayField {...props} />
```

In this example the array field itself would render, but the fields
within each row would not, because the array field did not pass its
permissions down to the rows.
2024-12-17 12:17:42 -05:00

259 lines
7.5 KiB
TypeScript

import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { createClientField, MissingEditorProp } from 'payload'
import { fieldIsHiddenOrDisabled } from 'payload/shared'
import type { RenderFieldMethod } from './types.js'
import { RenderServerComponent } from '../../elements/RenderServerComponent/index.js'
// eslint-disable-next-line payload/no-imports-from-exports-dir -- need this to reference already existing bundle. Otherwise, bundle size increases., payload/no-imports-from-exports-dir
import { FieldDescription, WatchCondition } from '../../exports/client/index.js'
const defaultUIFieldComponentKeys: Array<'Cell' | 'Description' | 'Field' | 'Filter'> = [
'Cell',
'Description',
'Field',
'Filter',
]
export const renderField: RenderFieldMethod = ({
id,
clientFieldSchemaMap,
collectionSlug,
data,
fieldConfig,
fieldSchemaMap,
fieldState,
formState,
indexPath,
operation,
parentPath,
parentSchemaPath,
path,
permissions,
preferences,
req,
schemaPath,
siblingData,
}) => {
const clientField = clientFieldSchemaMap
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
: createClientField({
defaultIDType: req.payload.config.db.defaultIDType,
field: fieldConfig,
i18n: req.i18n,
importMap: req.payload.importMap,
})
if (fieldIsHiddenOrDisabled(clientField)) {
return
}
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
customComponents: fieldState?.customComponents || {},
field: clientField,
path,
permissions,
readOnly: typeof permissions === 'boolean' ? !permissions : !permissions?.[operation],
schemaPath,
}
// fields with subfields
if (['array', 'blocks', 'collapsible', 'group', 'row', 'tabs'].includes(fieldConfig.type)) {
clientProps.indexPath = indexPath
clientProps.parentPath = parentPath
clientProps.parentSchemaPath = parentSchemaPath
}
const serverProps: ServerComponentProps = {
id,
clientField,
clientFieldSchemaMap,
data,
field: fieldConfig,
fieldSchemaMap,
permissions,
// TODO: Should we pass explicit values? initialValue, value, valid
// value and initialValue should be typed
collectionSlug,
formState,
i18n: req.i18n,
operation,
payload: req.payload,
preferences,
req,
siblingData,
user: req.user,
}
if (!fieldState?.customComponents) {
fieldState.customComponents = {}
}
switch (fieldConfig.type) {
// TODO: handle block row labels as well in a similar fashion
case 'array': {
fieldState?.rows?.forEach((row, rowIndex) => {
if (fieldConfig.admin?.components && 'RowLabel' in fieldConfig.admin.components) {
if (!fieldState.customComponents.RowLabels) {
fieldState.customComponents.RowLabels = []
}
fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.RowLabel,
importMap: req.payload.importMap,
serverProps: {
...serverProps,
rowLabel: `${getTranslation(fieldConfig.labels.singular, req.i18n)} ${String(
rowIndex + 1,
).padStart(2, '0')}`,
rowNumber: rowIndex + 1,
},
})
}
})
break
}
case 'richText': {
if (!fieldConfig?.editor) {
throw new MissingEditorProp(fieldConfig) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof fieldConfig?.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
if (!fieldConfig.admin) {
fieldConfig.admin = {}
}
if (!fieldConfig.admin.components) {
fieldConfig.admin.components = {}
}
fieldState.customComponents.Field = (
<WatchCondition path={path}>
{RenderServerComponent({
clientProps,
Component: fieldConfig.editor.FieldComponent,
importMap: req.payload.importMap,
serverProps,
})}
</WatchCondition>
)
break
}
case 'ui': {
if (fieldConfig?.admin?.components) {
// Render any extra, untyped components
for (const key in fieldConfig.admin.components) {
if (key in defaultUIFieldComponentKeys) {
continue
}
const Component = fieldConfig.admin.components[key]
fieldState.customComponents[key] = RenderServerComponent({
clientProps,
Component,
importMap: req.payload.importMap,
key: `field.admin.components.${key}`,
serverProps,
})
}
}
break
}
default: {
break
}
}
if (fieldConfig.admin) {
if ('description' in fieldConfig.admin) {
if (typeof fieldConfig.admin?.description === 'function') {
fieldState.customComponents.Description = (
<FieldDescription
description={fieldConfig.admin?.description({
t: req.i18n.t,
})}
path={path}
/>
)
}
}
if (fieldConfig.admin?.components) {
if ('afterInput' in fieldConfig.admin.components) {
fieldState.customComponents.AfterInput = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.afterInput,
importMap: req.payload.importMap,
key: 'field.admin.components.afterInput',
serverProps,
})
}
if ('beforeInput' in fieldConfig.admin.components) {
fieldState.customComponents.BeforeInput = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.beforeInput,
importMap: req.payload.importMap,
key: 'field.admin.components.beforeInput',
serverProps,
})
}
if ('Description' in fieldConfig.admin.components) {
fieldState.customComponents.Description = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Description,
importMap: req.payload.importMap,
key: 'field.admin.components.Description',
serverProps,
})
}
if ('Error' in fieldConfig.admin.components) {
fieldState.customComponents.Error = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Error,
importMap: req.payload.importMap,
key: 'field.admin.components.Error',
serverProps,
})
}
if ('Label' in fieldConfig.admin.components) {
fieldState.customComponents.Label = RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Label,
importMap: req.payload.importMap,
key: 'field.admin.components.Label',
serverProps,
})
}
if ('Field' in fieldConfig.admin.components) {
fieldState.customComponents.Field = (
<WatchCondition path={path}>
{RenderServerComponent({
clientProps,
Component: fieldConfig.admin.components.Field,
importMap: req.payload.importMap,
key: 'field.admin.components.Field',
serverProps,
})}
</WatchCondition>
)
}
}
}
}