Skip to content

Commit 6c19579

Browse files
authored
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.
1 parent 2bf58b6 commit 6c19579

File tree

11 files changed

+111
-23
lines changed

11 files changed

+111
-23
lines changed

packages/payload/src/admin/fields/Array.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { MarkOptional } from 'ts-essentials'
22

3-
import type { ArrayField, ArrayFieldClient, ClientField } from '../../fields/config/types.js'
3+
import type { ArrayField, ArrayFieldClient } from '../../fields/config/types.js'
44
import type { ArrayFieldValidation } from '../../fields/validations.js'
55
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
66
import type {

packages/payload/src/admin/fields/Blocks.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type React from 'react'
12
import type { MarkOptional } from 'ts-essentials'
23

34
import type { BlocksField, BlocksFieldClient } from '../../fields/config/types.js'
@@ -50,6 +51,20 @@ export type BlocksFieldLabelServerComponent = FieldLabelServerComponent<
5051
export type BlocksFieldLabelClientComponent =
5152
FieldLabelClientComponent<BlocksFieldClientWithoutType>
5253

54+
type BlockRowLabelBase = {
55+
blockType: string
56+
rowLabel: string
57+
rowNumber: number
58+
}
59+
60+
export type BlockRowLabelClientComponent = React.ComponentType<
61+
BlockRowLabelBase & ClientFieldBase<BlocksFieldClientWithoutType>
62+
>
63+
64+
export type BlockRowLabelServerComponent = React.ComponentType<
65+
BlockRowLabelBase & ServerFieldBase<BlocksField, BlocksFieldClientWithoutType>
66+
>
67+
5368
export type BlocksFieldDescriptionServerComponent = FieldDescriptionServerComponent<
5469
BlocksField,
5570
BlocksFieldClientWithoutType

packages/payload/src/admin/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export type {
6060
} from './fields/Array.js'
6161

6262
export type {
63+
BlockRowLabelClientComponent,
64+
BlockRowLabelServerComponent,
6365
BlocksFieldClientComponent,
6466
BlocksFieldClientProps,
6567
BlocksFieldDescriptionClientComponent,

packages/ui/src/fields/Blocks/BlockRow.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload'
33

44
import { getTranslation } from '@payloadcms/translations'
5-
import React from 'react'
5+
import React, { Fragment } from 'react'
66

77
import type { UseDraggableSortableReturn } from '../../elements/DraggableSortable/useDraggableSortable/types.js'
88
import type { RenderFieldsProps } from '../../forms/RenderFields/types.js'
@@ -143,21 +143,23 @@ export const BlockRow: React.FC<BlocksFieldProps> = ({
143143
isLoading ? (
144144
<ShimmerEffect height="1rem" width="8rem" />
145145
) : (
146-
Label || (
147-
<div className={`${baseClass}__block-header`}>
148-
<span className={`${baseClass}__block-number`}>
149-
{String(rowIndex + 1).padStart(2, '0')}
150-
</span>
151-
<Pill
152-
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
153-
pillStyle="white"
154-
>
155-
{getTranslation(block.labels.singular, i18n)}
156-
</Pill>
157-
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
158-
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
159-
</div>
160-
)
146+
<div className={`${baseClass}__block-header`}>
147+
{Label || (
148+
<Fragment>
149+
<span className={`${baseClass}__block-number`}>
150+
{String(rowIndex + 1).padStart(2, '0')}
151+
</span>
152+
<Pill
153+
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
154+
pillStyle="white"
155+
>
156+
{getTranslation(block.labels.singular, i18n)}
157+
</Pill>
158+
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
159+
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
160+
</Fragment>
161+
)}
162+
</div>
161163
)
162164
}
163165
isCollapsed={row.collapsed}

packages/ui/src/fields/Blocks/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
9595
)
9696

9797
const {
98-
customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {},
98+
customComponents: { AfterInput, BeforeInput, Description, Error, Label, RowLabels } = {},
9999
errorPaths,
100100
rows = [],
101101
showError,
@@ -283,7 +283,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
283283
hasMaxRows={hasMaxRows}
284284
isLoading={isLoading}
285285
isSortable={isSortable}
286-
Label={Label}
286+
Label={RowLabels?.[i]}
287287
labels={labels}
288288
moveRow={moveRow}
289289
parentPath={path}

packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ export const renderField: RenderFieldMethod = ({
106106
fieldState.customComponents = {}
107107
}
108108
}
109+
109110
switch (fieldConfig.type) {
110-
// TODO: handle block row labels as well in a similar fashion
111111
case 'array': {
112112
fieldState?.rows?.forEach((row, rowIndex) => {
113113
if (fieldConfig.admin?.components && 'RowLabel' in fieldConfig.admin.components) {
@@ -133,6 +133,38 @@ export const renderField: RenderFieldMethod = ({
133133
break
134134
}
135135

136+
case 'blocks': {
137+
fieldState?.rows?.forEach((row, rowIndex) => {
138+
const blockConfig = fieldConfig.blocks.find((block) => block.slug === row.blockType)
139+
140+
if (blockConfig.admin?.components && 'Label' in blockConfig.admin.components) {
141+
if (!fieldState.customComponents) {
142+
fieldState.customComponents = {}
143+
}
144+
145+
if (!fieldState.customComponents.RowLabels) {
146+
fieldState.customComponents.RowLabels = []
147+
}
148+
149+
fieldState.customComponents.RowLabels[rowIndex] = RenderServerComponent({
150+
clientProps,
151+
Component: blockConfig.admin.components.Label,
152+
importMap: req.payload.importMap,
153+
serverProps: {
154+
...serverProps,
155+
blockType: row.blockType,
156+
rowLabel: `${getTranslation(blockConfig.labels.singular, req.i18n)} ${String(
157+
rowIndex + 1,
158+
).padStart(2, '0')}`,
159+
rowNumber: rowIndex + 1,
160+
},
161+
})
162+
}
163+
})
164+
165+
break
166+
}
167+
136168
case 'richText': {
137169
if (!fieldConfig?.editor) {
138170
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
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { BlockRowLabelServerComponent } from 'payload'
2+
3+
const CustomBlockLabel: BlockRowLabelServerComponent = ({ rowLabel }) => {
4+
return <div>{`Custom Block Label: ${rowLabel}`}</div>
5+
}
6+
7+
export default CustomBlockLabel

test/fields/collections/Blocks/e2e.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,25 @@ describe('Block fields', () => {
168168
await expect(firstRow.locator('.blocks-field__block-pill-text')).toContainText('Text en')
169169
})
170170

171+
test('should render custom block row label', async () => {
172+
await page.goto(url.create)
173+
const addButton = page.locator('#field-blocks > .blocks-field__drawer-toggler')
174+
await addButton.click()
175+
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
176+
177+
await blocksDrawer
178+
.locator('.blocks-drawer__block .thumbnail-card__label', {
179+
hasText: 'Content',
180+
})
181+
.click()
182+
183+
await expect(
184+
await page.locator('#field-blocks .blocks-field__row .blocks-field__block-header', {
185+
hasText: 'Custom Block Label',
186+
}),
187+
).toBeVisible()
188+
})
189+
171190
test('should add different blocks with similar field configs', async () => {
172191
await page.goto(url.create)
173192

test/fields/collections/Blocks/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export const getBlocksField = (prefix?: string): BlocksField => ({
1212
{
1313
slug: prefix ? `${prefix}Content` : 'content',
1414
interfaceName: prefix ? `${prefix}ContentBlock` : 'ContentBlock',
15+
admin: {
16+
components: {
17+
Label: './collections/Blocks/components/CustomBlockLabel.tsx',
18+
},
19+
},
1520
fields: [
1621
{
1722
name: 'text',

test/fields/payload-types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ export interface ArrayField {
466466
subArray?:
467467
| {
468468
text?: string | null;
469+
textTwo: string;
470+
textInRow: string;
469471
id?: string | null;
470472
}[]
471473
| null;
@@ -878,10 +880,11 @@ export interface CodeField {
878880
export interface CollapsibleField {
879881
id: string;
880882
text: string;
881-
group?: {
883+
group: {
882884
textWithinGroup?: string | null;
883-
subGroup?: {
885+
subGroup: {
884886
textWithinSubGroup?: string | null;
887+
requiredTextWithinSubGroup: string;
885888
};
886889
};
887890
someText?: string | null;
@@ -2087,6 +2090,8 @@ export interface ArrayFieldsSelect<T extends boolean = true> {
20872090
| T
20882091
| {
20892092
text?: T;
2093+
textTwo?: T;
2094+
textInRow?: T;
20902095
id?: T;
20912096
};
20922097
id?: T;
@@ -2490,6 +2495,7 @@ export interface CollapsibleFieldsSelect<T extends boolean = true> {
24902495
| T
24912496
| {
24922497
textWithinSubGroup?: T;
2498+
requiredTextWithinSubGroup?: T;
24932499
};
24942500
};
24952501
someText?: T;

0 commit comments

Comments
 (0)