fix: error when passing functions to array or block fields labels property (#11056)

Fixes https://github.com/payloadcms/payload/issues/11055

Functions passed to array field, block field or block `labels` were not properly handled in the client config, causing those functions to be sent to the client. This leads to a "Functions cannot be passed directly to Client Component" error
This commit is contained in:
Alessio Gravili
2025-02-07 14:52:01 -07:00
committed by GitHub
parent d7a7fbf93a
commit 6d48cf9bbf
7 changed files with 179 additions and 2 deletions

View File

@@ -1,7 +1,10 @@
/* eslint-disable perfectionist/sort-switch-case */
// Keep perfectionist/sort-switch-case disabled - it incorrectly messes up the ordering of the switch cases, causing it to break
import type { I18nClient } from '@payloadcms/translations' import type { I18nClient } from '@payloadcms/translations'
import type { import type {
AdminClient, AdminClient,
ArrayFieldClient,
BlockJSX, BlockJSX,
BlocksFieldClient, BlocksFieldClient,
ClientBlock, ClientBlock,
@@ -144,7 +147,27 @@ export const createClientField = ({
} }
switch (incomingField.type) { switch (incomingField.type) {
case 'array': case 'array': {
if (incomingField.labels) {
const field = clientField as unknown as ArrayFieldClient
field.labels = {} as unknown as LabelsClient
if (incomingField.labels.singular) {
if (typeof incomingField.labels.singular === 'function') {
field.labels.singular = incomingField.labels.singular({ t: i18n.t })
} else {
field.labels.singular = incomingField.labels.singular
}
if (typeof incomingField.labels.plural === 'function') {
field.labels.plural = incomingField.labels.plural({ t: i18n.t })
} else {
field.labels.plural = incomingField.labels.plural
}
}
}
}
// falls through
case 'collapsible': case 'collapsible':
case 'group': case 'group':
case 'row': { case 'row': {
@@ -168,6 +191,23 @@ export const createClientField = ({
case 'blocks': { case 'blocks': {
const field = clientField as unknown as BlocksFieldClient const field = clientField as unknown as BlocksFieldClient
if (incomingField.labels) {
field.labels = {} as unknown as LabelsClient
if (incomingField.labels.singular) {
if (typeof incomingField.labels.singular === 'function') {
field.labels.singular = incomingField.labels.singular({ t: i18n.t })
} else {
field.labels.singular = incomingField.labels.singular
}
if (typeof incomingField.labels.plural === 'function') {
field.labels.plural = incomingField.labels.plural({ t: i18n.t })
} else {
field.labels.plural = incomingField.labels.plural
}
}
}
if (incomingField.blocks?.length) { if (incomingField.blocks?.length) {
for (let i = 0; i < incomingField.blocks.length; i++) { for (let i = 0; i < incomingField.blocks.length; i++) {
const block = incomingField.blocks[i] const block = incomingField.blocks[i]

View File

@@ -13,6 +13,7 @@ export interface Config {
collections: { collections: {
posts: Post; posts: Post;
media: Media; media: Media;
test: Test;
users: User; users: User;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
@@ -22,6 +23,7 @@ export interface Config {
collectionsSelect: { collectionsSelect: {
posts: PostsSelect<false> | PostsSelect<true>; posts: PostsSelect<false> | PostsSelect<true>;
media: MediaSelect<false> | MediaSelect<true>; media: MediaSelect<false> | MediaSelect<true>;
test: TestSelect<false> | TestSelect<true>;
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -133,6 +135,28 @@ export interface Media {
}; };
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "test".
*/
export interface Test {
id: string;
test_array?:
| {
test_text?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'test_text';
}[]
| null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users". * via the `definition` "users".
@@ -165,6 +189,10 @@ export interface PayloadLockedDocument {
relationTo: 'media'; relationTo: 'media';
value: string | Media; value: string | Media;
} | null) } | null)
| ({
relationTo: 'test';
value: string | Test;
} | null)
| ({ | ({
relationTo: 'users'; relationTo: 'users';
value: string | User; value: string | User;
@@ -273,6 +301,30 @@ export interface MediaSelect<T extends boolean = true> {
}; };
}; };
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "test_select".
*/
export interface TestSelect<T extends boolean = true> {
test_array?:
| T
| {
test_text?:
| T
| {
test_text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select". * via the `definition` "users_select".

View File

@@ -133,6 +133,13 @@ describe('Array', () => {
await expect(page.locator('#field-items #items-row-0 .row-label')).toContainText('Item 01') await expect(page.locator('#field-items #items-row-0 .row-label')).toContainText('Item 01')
}) })
test('ensure functions passed to array field labels property are respected', async () => {
await page.goto(url.create)
const arrayWithLabelsField = page.locator('#field-arrayWithLabels')
await expect(arrayWithLabelsField.locator('.array-field__add-row')).toHaveText('Add Account')
})
describe('row manipulation', () => { describe('row manipulation', () => {
test('should add, remove and duplicate rows', async () => { test('should add, remove and duplicate rows', async () => {
const assertText0 = 'array row 1' const assertText0 = 'array row 1'

View File

@@ -246,6 +246,20 @@ const ArrayFields: CollectionConfig = {
}, },
}, },
}, },
{
name: 'arrayWithLabels',
type: 'array',
labels: {
singular: ({ t }) => t('authentication:account'),
plural: ({ t }) => t('authentication:generate'),
},
fields: [
{
name: 'text',
type: 'text',
},
],
},
], ],
slug: arrayFieldsSlug, slug: arrayFieldsSlug,
versions: true, versions: true,

View File

@@ -212,7 +212,7 @@ describe('Block fields', () => {
.click() .click()
await expect( await expect(
await page.locator('#field-blocks .blocks-field__row .blocks-field__block-header', { page.locator('#field-blocks .blocks-field__row .blocks-field__block-header', {
hasText: 'Custom Block Label', hasText: 'Custom Block Label',
}), }),
).toBeVisible() ).toBeVisible()
@@ -292,6 +292,16 @@ describe('Block fields', () => {
) )
}) })
test('ensure functions passed to blocks field labels property are respected', async () => {
await page.goto(url.create)
const blocksFieldWithLabels = page.locator('#field-blockWithLabels')
await expect(blocksFieldWithLabels.locator('.blocks-field__drawer-toggler')).toHaveText(
'Add Account',
)
})
describe('row manipulation', () => { describe('row manipulation', () => {
describe('react hooks', () => { describe('react hooks', () => {
test('should add 2 new block rows', async () => { test('should add 2 new block rows', async () => {

View File

@@ -373,6 +373,29 @@ const BlockFields: CollectionConfig = {
}, },
], ],
}, },
{
name: 'blockWithLabels',
type: 'blocks',
labels: {
singular: ({ t }) => t('authentication:account'),
plural: ({ t }) => t('authentication:generate'),
},
blocks: [
{
labels: {
singular: ({ t }) => t('authentication:account'),
plural: ({ t }) => t('authentication:generate'),
},
slug: 'text',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
},
], ],
} }

View File

@@ -595,6 +595,12 @@ export interface ArrayField {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
arrayWithLabels?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -727,6 +733,14 @@ export interface BlockField {
blockType: 'relationships'; blockType: 'relationships';
}[] }[]
| null; | null;
blockWithLabels?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'text';
}[]
| null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -2265,6 +2279,12 @@ export interface ArrayFieldsSelect<T extends boolean = true> {
text?: T; text?: T;
id?: T; id?: T;
}; };
arrayWithLabels?:
| T
| {
text?: T;
id?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
@@ -2446,6 +2466,17 @@ export interface BlockFieldsSelect<T extends boolean = true> {
blockName?: T; blockName?: T;
}; };
}; };
blockWithLabels?:
| T
| {
text?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }