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.
This commit is contained in:
Jacob Fletcher
2024-12-17 12:17:42 -05:00
committed by GitHub
parent 99ca1babc6
commit eb037a0cc6
14 changed files with 218 additions and 316 deletions

View File

@@ -21,8 +21,7 @@ type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
type ArrayFieldBaseClientProps = {
readonly validate?: ArrayFieldValidation
} & FieldPaths &
Pick<ServerFieldBase, 'permissions'>
} & FieldPaths
export type ArrayFieldClientProps = ArrayFieldBaseClientProps &
ClientFieldBase<ArrayFieldClientWithoutType>

View File

@@ -21,8 +21,7 @@ type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
type BlocksFieldBaseClientProps = {
readonly validate?: BlocksFieldValidation
} & FieldPaths &
Pick<ServerFieldBase, 'permissions'>
} & FieldPaths
export type BlocksFieldClientProps = BlocksFieldBaseClientProps &
ClientFieldBase<BlocksFieldClientWithoutType>

View File

@@ -16,7 +16,7 @@ import type {
FieldLabelServerComponent,
} from '../types.js'
type CollapsibleFieldBaseClientProps = FieldPaths & Pick<ServerFieldBase, 'permissions'>
type CollapsibleFieldBaseClientProps = FieldPaths
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>

View File

@@ -18,7 +18,7 @@ import type {
type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, 'type'>
export type GroupFieldBaseClientProps = FieldPaths & Pick<ServerFieldBase, 'permissions'>
export type GroupFieldBaseClientProps = FieldPaths
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType> &
GroupFieldBaseClientProps

View File

@@ -21,8 +21,7 @@ type RowFieldClientWithoutType = MarkOptional<RowFieldClient, 'type'>
type RowFieldBaseClientProps = {
readonly forceRender?: boolean
} & Omit<FieldPaths, 'path'> &
Pick<ServerFieldBase, 'permissions'>
} & Omit<FieldPaths, 'path'>
export type RowFieldClientProps = Omit<ClientFieldBase<RowFieldClientWithoutType>, 'path'> &
RowFieldBaseClientProps

View File

@@ -26,7 +26,7 @@ export type ClientTab =
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
type TabsFieldBaseClientProps = FieldPaths & Pick<ServerFieldBase, 'permissions'>
type TabsFieldBaseClientProps = FieldPaths
type TabsFieldClientWithoutType = MarkOptional<TabsFieldClient, 'type'>

View File

@@ -21,6 +21,7 @@ export type ClientComponentProps = {
customComponents: FormField['customComponents']
field: ClientBlock | ClientField | ClientTab
forceRender?: boolean
permissions?: SanitizedFieldPermissions
readOnly?: boolean
renderedBlocks?: RenderedField[]
/**

View File

@@ -57,8 +57,12 @@ export function RenderField({
return CustomField || null
}
const baseFieldProps: Pick<ClientComponentProps, 'forceRender' | 'readOnly' | 'schemaPath'> = {
const baseFieldProps: Pick<
ClientComponentProps,
'forceRender' | 'permissions' | 'readOnly' | 'schemaPath'
> = {
forceRender,
permissions,
readOnly,
schemaPath,
}
@@ -68,7 +72,6 @@ export function RenderField({
indexPath,
parentPath,
parentSchemaPath,
permissions,
}
if (clientFieldConfig.admin?.hidden) {

View File

@@ -55,6 +55,7 @@ export const renderField: RenderFieldMethod = ({
customComponents: fieldState?.customComponents || {},
field: clientField,
path,
permissions,
readOnly: typeof permissions === 'boolean' ? !permissions : !permissions?.[operation],
schemaPath,
}

View File

@@ -0,0 +1,12 @@
'use client'
import type { ArrayFieldClientComponent } from 'payload'
import { ArrayField } from '@payloadcms/ui'
export const CustomArrayField: ArrayFieldClientComponent = (props) => {
return (
<div id="custom-array-field">
<ArrayField {...props} />
</div>
)
}

View File

@@ -2,9 +2,9 @@ import type { TextFieldServerComponent } from 'payload'
import { TextField } from '@payloadcms/ui'
export const CustomField: TextFieldServerComponent = ({ clientField, path }) => {
export const CustomTextField: TextFieldServerComponent = ({ clientField, path }) => {
return (
<div id="custom-field">
<div id="custom-text-field">
<TextField field={clientField} path={path as string} />
</div>
)

View File

@@ -106,6 +106,12 @@ describe('Array', () => {
await expect(customRowLabel).toHaveCSS('text-transform', 'uppercase')
})
test('should render default array field within custom component', async () => {
await page.goto(url.create)
await page.locator('#field-customArrayField >> .array-field__add-row').click()
await expect(page.locator('#field-customArrayField__0__text')).toBeVisible()
})
// eslint-disable-next-line playwright/expect-expect
test('should bypass min rows validation when no rows present and field is not required', async () => {
await page.goto(url.create)
@@ -313,7 +319,7 @@ describe('Array', () => {
test('should externally update array rows and render custom fields', async () => {
await page.goto(url.create)
await page.locator('#updateArrayExternally').click()
await expect(page.locator('#custom-field')).toBeVisible()
await expect(page.locator('#custom-text-field')).toBeVisible()
})
test('should not re-close initCollapsed true array rows on input in create new view', async () => {

View File

@@ -194,16 +194,31 @@ const ArrayFields: CollectionConfig = {
type: 'array',
fields: [
{
name: 'customField',
name: 'customTextField',
type: 'ui',
admin: {
components: {
Field: '/collections/Array/CustomField.js#CustomField',
Field: '/collections/Array/CustomTextField.js#CustomTextField',
},
},
},
],
},
{
name: 'customArrayField',
type: 'array',
admin: {
components: {
Field: '/collections/Array/CustomArrayField.js#CustomArrayField',
},
},
fields: [
{
name: 'text',
type: 'text',
},
],
},
{
name: 'ui',
type: 'ui',

View File

@@ -505,6 +505,12 @@ export interface ArrayField {
id?: string | null;
}[]
| null;
customArrayField?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
@@ -2060,6 +2066,12 @@ export interface ArrayFieldsSelect<T extends boolean = true> {
| {
id?: T;
};
customArrayField?:
| T
| {
text?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
@@ -2071,257 +2083,42 @@ export interface BlockFieldsSelect<T extends boolean = true> {
blocks?:
| T
| {
content?:
| T
| {
text?: T;
richText?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
subBlocks?:
| T
| {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
};
tabs?:
| T
| {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
};
content?: T | ContentBlockSelect<T>;
number?: T | NumberBlockSelect<T>;
subBlocks?: T | SubBlocksBlockSelect<T>;
tabs?: T | TabsBlockSelect<T>;
};
duplicate?:
| T
| {
content?:
| T
| {
text?: T;
richText?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
subBlocks?:
| T
| {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
};
tabs?:
| T
| {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
};
content?: T | ContentBlockSelect<T>;
number?: T | NumberBlockSelect<T>;
subBlocks?: T | SubBlocksBlockSelect<T>;
tabs?: T | TabsBlockSelect<T>;
};
collapsedByDefaultBlocks?:
| T
| {
localizedContent?:
| T
| {
text?: T;
richText?: T;
id?: T;
blockName?: T;
};
localizedNumber?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
localizedSubBlocks?:
| T
| {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
};
localizedTabs?:
| T
| {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
};
localizedContent?: T | LocalizedContentBlockSelect<T>;
localizedNumber?: T | LocalizedNumberBlockSelect<T>;
localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect<T>;
localizedTabs?: T | LocalizedTabsBlockSelect<T>;
};
disableSort?:
| T
| {
localizedContent?:
| T
| {
text?: T;
richText?: T;
id?: T;
blockName?: T;
};
localizedNumber?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
localizedSubBlocks?:
| T
| {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
};
localizedTabs?:
| T
| {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
};
localizedContent?: T | LocalizedContentBlockSelect<T>;
localizedNumber?: T | LocalizedNumberBlockSelect<T>;
localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect<T>;
localizedTabs?: T | LocalizedTabsBlockSelect<T>;
};
localizedBlocks?:
| T
| {
localizedContent?:
| T
| {
text?: T;
richText?: T;
id?: T;
blockName?: T;
};
localizedNumber?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
localizedSubBlocks?:
| T
| {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
};
localizedTabs?:
| T
| {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
};
localizedContent?: T | LocalizedContentBlockSelect<T>;
localizedNumber?: T | LocalizedNumberBlockSelect<T>;
localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect<T>;
localizedTabs?: T | LocalizedTabsBlockSelect<T>;
};
i18nBlocks?:
| T
@@ -2459,6 +2256,116 @@ export interface BlockFieldsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "ContentBlock_select".
*/
export interface ContentBlockSelect<T extends boolean = true> {
text?: T;
richText?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "NumberBlock_select".
*/
export interface NumberBlockSelect<T extends boolean = true> {
number?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "SubBlocksBlock_select".
*/
export interface SubBlocksBlockSelect<T extends boolean = true> {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TabsBlock_select".
*/
export interface TabsBlockSelect<T extends boolean = true> {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localizedContentBlock_select".
*/
export interface LocalizedContentBlockSelect<T extends boolean = true> {
text?: T;
richText?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localizedNumberBlock_select".
*/
export interface LocalizedNumberBlockSelect<T extends boolean = true> {
number?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localizedSubBlocksBlock_select".
*/
export interface LocalizedSubBlocksBlockSelect<T extends boolean = true> {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localizedTabsBlock_select".
*/
export interface LocalizedTabsBlockSelect<T extends boolean = true> {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "checkbox-fields_select".
@@ -3021,53 +2928,10 @@ export interface TabsFieldsSelect<T extends boolean = true> {
blocks?:
| T
| {
content?:
| T
| {
text?: T;
richText?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
subBlocks?:
| T
| {
subBlocks?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
number?:
| T
| {
number?: T;
id?: T;
blockName?: T;
};
};
id?: T;
blockName?: T;
};
tabs?:
| T
| {
textInCollapsible?: T;
textInRow?: T;
id?: T;
blockName?: T;
};
content?: T | ContentBlockSelect<T>;
number?: T | NumberBlockSelect<T>;
subBlocks?: T | SubBlocksBlockSelect<T>;
tabs?: T | TabsBlockSelect<T>;
};
group?:
| T
@@ -3077,24 +2941,7 @@ export interface TabsFieldsSelect<T extends boolean = true> {
textInRow?: T;
numberInRow?: T;
json?: T;
tab?:
| T
| {
array?:
| T
| {
text?: T;
id?: T;
};
text?: T;
defaultValue?: T;
arrayInRow?:
| T
| {
textInArrayInRow?: T;
id?: T;
};
};
tab?: T | TabWithNameSelect<T>;
namedTabWithDefaultValue?:
| T
| {
@@ -3144,6 +2991,26 @@ export interface TabsFieldsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TabWithName_select".
*/
export interface TabWithNameSelect<T extends boolean = true> {
array?:
| T
| {
text?: T;
id?: T;
};
text?: T;
defaultValue?: T;
arrayInRow?:
| T
| {
textInArrayInRow?: T;
id?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "text-fields_select".