fix(ui): renders custom block row labels (#10686)

Custom block row labels defined on `admin.components.Label` were not
rendering despite existing in the config. Instead, if a custom label
component was defined on the _top-level_ blocks field itself, it was
incorrectly replacing each blocks label _in addition_ to the field's
label. Now, custom labels defined at the field-level now only replace
the field's label as expected, and custom labels defined at the
block-level are now supported as the types suggest.
This commit is contained in:
Jacob Fletcher
2025-01-20 15:04:19 -05:00
committed by GitHub
parent 2bf58b6ac5
commit 6c19579ccf
11 changed files with 111 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
import type { MarkOptional } from 'ts-essentials'
import type { ArrayField, ArrayFieldClient, ClientField } from '../../fields/config/types.js'
import type { ArrayField, ArrayFieldClient } from '../../fields/config/types.js'
import type { ArrayFieldValidation } from '../../fields/validations.js'
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
import type {

View File

@@ -1,3 +1,4 @@
import type React from 'react'
import type { MarkOptional } from 'ts-essentials'
import type { BlocksField, BlocksFieldClient } from '../../fields/config/types.js'
@@ -50,6 +51,20 @@ export type BlocksFieldLabelServerComponent = FieldLabelServerComponent<
export type BlocksFieldLabelClientComponent =
FieldLabelClientComponent<BlocksFieldClientWithoutType>
type BlockRowLabelBase = {
blockType: string
rowLabel: string
rowNumber: number
}
export type BlockRowLabelClientComponent = React.ComponentType<
BlockRowLabelBase & ClientFieldBase<BlocksFieldClientWithoutType>
>
export type BlockRowLabelServerComponent = React.ComponentType<
BlockRowLabelBase & ServerFieldBase<BlocksField, BlocksFieldClientWithoutType>
>
export type BlocksFieldDescriptionServerComponent = FieldDescriptionServerComponent<
BlocksField,
BlocksFieldClientWithoutType

View File

@@ -60,6 +60,8 @@ export type {
} from './fields/Array.js'
export type {
BlockRowLabelClientComponent,
BlockRowLabelServerComponent,
BlocksFieldClientComponent,
BlocksFieldClientProps,
BlocksFieldDescriptionClientComponent,

View File

@@ -2,7 +2,7 @@
import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import React, { Fragment } from 'react'
import type { UseDraggableSortableReturn } from '../../elements/DraggableSortable/useDraggableSortable/types.js'
import type { RenderFieldsProps } from '../../forms/RenderFields/types.js'
@@ -143,8 +143,9 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
isLoading ? (
<ShimmerEffect height="1rem" width="8rem" />
) : (
Label || (
<div className={`${baseClass}__block-header`}>
{Label || (
<Fragment>
<span className={`${baseClass}__block-number`}>
{String(rowIndex + 1).padStart(2, '0')}
</span>
@@ -156,9 +157,10 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
</Pill>
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</Fragment>
)}
</div>
)
)
}
isCollapsed={row.collapsed}
key={row.id}

View File

@@ -95,7 +95,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
)
const {
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
errorPaths,
rows = [],
showError,
@@ -283,7 +283,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
hasMaxRows={hasMaxRows}
isLoading={isLoading}
isSortable={isSortable}
Label={Label}
Label={RowLabels?.[i]}
labels={labels}
moveRow={moveRow}
parentPath={path}

View File

@@ -106,8 +106,8 @@ export const renderField: RenderFieldMethod = ({
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) {
@@ -133,6 +133,38 @@ export const renderField: RenderFieldMethod = ({
break
}
case 'blocks': {
fieldState?.rows?.forEach((row, rowIndex) => {
const blockConfig = fieldConfig.blocks.find((block) => block.slug === row.blockType)
if (blockConfig.admin?.components && 'Label' in blockConfig.admin.components) {
if (!fieldState.customComponents) {
fieldState.customComponents = {}
}
if (!fieldState.customComponents.RowLabels) {
fieldState.customComponents.RowLabels = []
}
fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
clientProps,
Component: blockConfig.admin.components.Label,
importMap: req.payload.importMap,
serverProps: {
...serverProps,
blockType: row.blockType,
rowLabel: `${getTranslation(blockConfig.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

View File

@@ -0,0 +1,7 @@
import type { BlockRowLabelServerComponent } from 'payload'
const CustomBlockLabel: BlockRowLabelServerComponent = ({ rowLabel }) => {
return <div>{`Custom Block Label: ${rowLabel}`}</div>
}
export default CustomBlockLabel

View File

@@ -168,6 +168,25 @@ describe('Block fields', () => {
await expect(firstRow.locator('.blocks-field__block-pill-text')).toContainText('Text en')
})
test('should render custom block row label', async () => {
await page.goto(url.create)
const addButton = page.locator('#field-blocks > .blocks-field__drawer-toggler')
await addButton.click()
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
await blocksDrawer
.locator('.blocks-drawer__block .thumbnail-card__label', {
hasText: 'Content',
})
.click()
await expect(
await page.locator('#field-blocks .blocks-field__row .blocks-field__block-header', {
hasText: 'Custom Block Label',
}),
).toBeVisible()
})
test('should add different blocks with similar field configs', async () => {
await page.goto(url.create)

View File

@@ -12,6 +12,11 @@ export const getBlocksField = (prefix?: string): BlocksField => ({
{
slug: prefix ? `${prefix}Content` : 'content',
interfaceName: prefix ? `${prefix}ContentBlock` : 'ContentBlock',
admin: {
components: {
Label: './collections/Blocks/components/CustomBlockLabel.tsx',
},
},
fields: [
{
name: 'text',

View File

@@ -466,6 +466,8 @@ export interface ArrayField {
subArray?:
| {
text?: string | null;
textTwo: string;
textInRow: string;
id?: string | null;
}[]
| null;
@@ -878,10 +880,11 @@ export interface CodeField {
export interface CollapsibleField {
id: string;
text: string;
group?: {
group: {
textWithinGroup?: string | null;
subGroup?: {
subGroup: {
textWithinSubGroup?: string | null;
requiredTextWithinSubGroup: string;
};
};
someText?: string | null;
@@ -2087,6 +2090,8 @@ export interface ArrayFieldsSelect<T extends boolean = true> {
| T
| {
text?: T;
textTwo?: T;
textInRow?: T;
id?: T;
};
id?: T;
@@ -2490,6 +2495,7 @@ export interface CollapsibleFieldsSelect<T extends boolean = true> {
| T
| {
textWithinSubGroup?: T;
requiredTextWithinSubGroup?: T;
};
};
someText?: T;

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/_community/config.ts"],
"@payload-config": ["./test/fields/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],