From eb037a0cc637d49d9d8451d37b5ca10399670a9b Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 17 Dec 2024 12:17:42 -0500 Subject: [PATCH] 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) => ``` 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. --- packages/payload/src/admin/fields/Array.ts | 3 +- packages/payload/src/admin/fields/Blocks.ts | 3 +- .../payload/src/admin/fields/Collapsible.ts | 2 +- packages/payload/src/admin/fields/Group.ts | 2 +- packages/payload/src/admin/fields/Row.ts | 3 +- packages/payload/src/admin/fields/Tabs.ts | 2 +- packages/payload/src/admin/forms/Field.ts | 1 + .../ui/src/forms/RenderFields/RenderField.tsx | 7 +- .../fieldSchemasToFormState/renderField.tsx | 1 + .../collections/Array/CustomArrayField.tsx | 12 + .../{CustomField.tsx => CustomTextField.tsx} | 4 +- test/fields/collections/Array/e2e.spec.ts | 8 +- test/fields/collections/Array/index.ts | 19 +- test/fields/payload-types.ts | 467 +++++++----------- 14 files changed, 218 insertions(+), 316 deletions(-) create mode 100644 test/fields/collections/Array/CustomArrayField.tsx rename test/fields/collections/Array/{CustomField.tsx => CustomTextField.tsx} (61%) diff --git a/packages/payload/src/admin/fields/Array.ts b/packages/payload/src/admin/fields/Array.ts index d67c0c315a..603dcaf3d6 100644 --- a/packages/payload/src/admin/fields/Array.ts +++ b/packages/payload/src/admin/fields/Array.ts @@ -21,8 +21,7 @@ type ArrayFieldClientWithoutType = MarkOptional type ArrayFieldBaseClientProps = { readonly validate?: ArrayFieldValidation -} & FieldPaths & - Pick +} & FieldPaths export type ArrayFieldClientProps = ArrayFieldBaseClientProps & ClientFieldBase diff --git a/packages/payload/src/admin/fields/Blocks.ts b/packages/payload/src/admin/fields/Blocks.ts index 921005a03f..0c379d0ff3 100644 --- a/packages/payload/src/admin/fields/Blocks.ts +++ b/packages/payload/src/admin/fields/Blocks.ts @@ -21,8 +21,7 @@ type BlocksFieldClientWithoutType = MarkOptional type BlocksFieldBaseClientProps = { readonly validate?: BlocksFieldValidation -} & FieldPaths & - Pick +} & FieldPaths export type BlocksFieldClientProps = BlocksFieldBaseClientProps & ClientFieldBase diff --git a/packages/payload/src/admin/fields/Collapsible.ts b/packages/payload/src/admin/fields/Collapsible.ts index 01b90fa4ce..94581cef49 100644 --- a/packages/payload/src/admin/fields/Collapsible.ts +++ b/packages/payload/src/admin/fields/Collapsible.ts @@ -16,7 +16,7 @@ import type { FieldLabelServerComponent, } from '../types.js' -type CollapsibleFieldBaseClientProps = FieldPaths & Pick +type CollapsibleFieldBaseClientProps = FieldPaths type CollapsibleFieldClientWithoutType = MarkOptional diff --git a/packages/payload/src/admin/fields/Group.ts b/packages/payload/src/admin/fields/Group.ts index deda3081dc..cabbff5077 100644 --- a/packages/payload/src/admin/fields/Group.ts +++ b/packages/payload/src/admin/fields/Group.ts @@ -18,7 +18,7 @@ import type { type GroupFieldClientWithoutType = MarkOptional -export type GroupFieldBaseClientProps = FieldPaths & Pick +export type GroupFieldBaseClientProps = FieldPaths export type GroupFieldClientProps = ClientFieldBase & GroupFieldBaseClientProps diff --git a/packages/payload/src/admin/fields/Row.ts b/packages/payload/src/admin/fields/Row.ts index 9a179b3e71..0e508981b9 100644 --- a/packages/payload/src/admin/fields/Row.ts +++ b/packages/payload/src/admin/fields/Row.ts @@ -21,8 +21,7 @@ type RowFieldClientWithoutType = MarkOptional type RowFieldBaseClientProps = { readonly forceRender?: boolean -} & Omit & - Pick +} & Omit export type RowFieldClientProps = Omit, 'path'> & RowFieldBaseClientProps diff --git a/packages/payload/src/admin/fields/Tabs.ts b/packages/payload/src/admin/fields/Tabs.ts index 2cbb4bbd97..76e992596a 100644 --- a/packages/payload/src/admin/fields/Tabs.ts +++ b/packages/payload/src/admin/fields/Tabs.ts @@ -26,7 +26,7 @@ export type ClientTab = | ({ fields: ClientField[]; readonly path?: string } & Omit) | ({ fields: ClientField[] } & Omit) -type TabsFieldBaseClientProps = FieldPaths & Pick +type TabsFieldBaseClientProps = FieldPaths type TabsFieldClientWithoutType = MarkOptional diff --git a/packages/payload/src/admin/forms/Field.ts b/packages/payload/src/admin/forms/Field.ts index 1157f4d3e9..93c2e7fb47 100644 --- a/packages/payload/src/admin/forms/Field.ts +++ b/packages/payload/src/admin/forms/Field.ts @@ -21,6 +21,7 @@ export type ClientComponentProps = { customComponents: FormField['customComponents'] field: ClientBlock | ClientField | ClientTab forceRender?: boolean + permissions?: SanitizedFieldPermissions readOnly?: boolean renderedBlocks?: RenderedField[] /** diff --git a/packages/ui/src/forms/RenderFields/RenderField.tsx b/packages/ui/src/forms/RenderFields/RenderField.tsx index a754e1f122..9c13b8541e 100644 --- a/packages/ui/src/forms/RenderFields/RenderField.tsx +++ b/packages/ui/src/forms/RenderFields/RenderField.tsx @@ -57,8 +57,12 @@ export function RenderField({ return CustomField || null } - const baseFieldProps: Pick = { + 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) { diff --git a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx index 9baafc725f..deca48d7db 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx @@ -55,6 +55,7 @@ export const renderField: RenderFieldMethod = ({ customComponents: fieldState?.customComponents || {}, field: clientField, path, + permissions, readOnly: typeof permissions === 'boolean' ? !permissions : !permissions?.[operation], schemaPath, } diff --git a/test/fields/collections/Array/CustomArrayField.tsx b/test/fields/collections/Array/CustomArrayField.tsx new file mode 100644 index 0000000000..adad96b297 --- /dev/null +++ b/test/fields/collections/Array/CustomArrayField.tsx @@ -0,0 +1,12 @@ +'use client' +import type { ArrayFieldClientComponent } from 'payload' + +import { ArrayField } from '@payloadcms/ui' + +export const CustomArrayField: ArrayFieldClientComponent = (props) => { + return ( +
+ +
+ ) +} diff --git a/test/fields/collections/Array/CustomField.tsx b/test/fields/collections/Array/CustomTextField.tsx similarity index 61% rename from test/fields/collections/Array/CustomField.tsx rename to test/fields/collections/Array/CustomTextField.tsx index 6119d02322..091fcf4591 100644 --- a/test/fields/collections/Array/CustomField.tsx +++ b/test/fields/collections/Array/CustomTextField.tsx @@ -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 ( -
+
) diff --git a/test/fields/collections/Array/e2e.spec.ts b/test/fields/collections/Array/e2e.spec.ts index ff4ab5f396..5b3c704dcb 100644 --- a/test/fields/collections/Array/e2e.spec.ts +++ b/test/fields/collections/Array/e2e.spec.ts @@ -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 () => { diff --git a/test/fields/collections/Array/index.ts b/test/fields/collections/Array/index.ts index dad95be90d..df1abd2d15 100644 --- a/test/fields/collections/Array/index.ts +++ b/test/fields/collections/Array/index.ts @@ -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', diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 33c5319405..69aca10d18 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -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 { | { id?: T; }; + customArrayField?: + | T + | { + text?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; } @@ -2071,257 +2083,42 @@ export interface BlockFieldsSelect { 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; + number?: T | NumberBlockSelect; + subBlocks?: T | SubBlocksBlockSelect; + tabs?: T | TabsBlockSelect; }; 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; + number?: T | NumberBlockSelect; + subBlocks?: T | SubBlocksBlockSelect; + tabs?: T | TabsBlockSelect; }; 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; + localizedNumber?: T | LocalizedNumberBlockSelect; + localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; + localizedTabs?: T | LocalizedTabsBlockSelect; }; 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; + localizedNumber?: T | LocalizedNumberBlockSelect; + localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; + localizedTabs?: T | LocalizedTabsBlockSelect; }; 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; + localizedNumber?: T | LocalizedNumberBlockSelect; + localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; + localizedTabs?: T | LocalizedTabsBlockSelect; }; i18nBlocks?: | T @@ -2459,6 +2256,116 @@ export interface BlockFieldsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "ContentBlock_select". + */ +export interface ContentBlockSelect { + 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 { + number?: T; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "SubBlocksBlock_select". + */ +export interface SubBlocksBlockSelect { + 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 { + 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 { + 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 { + number?: T; + id?: T; + blockName?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedSubBlocksBlock_select". + */ +export interface LocalizedSubBlocksBlockSelect { + 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 { + 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 { 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; + number?: T | NumberBlockSelect; + subBlocks?: T | SubBlocksBlockSelect; + tabs?: T | TabsBlockSelect; }; group?: | T @@ -3077,24 +2941,7 @@ export interface TabsFieldsSelect { 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; namedTabWithDefaultValue?: | T | { @@ -3144,6 +2991,26 @@ export interface TabsFieldsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "TabWithName_select". + */ +export interface TabWithNameSelect { + 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".