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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,8 @@ export type {
|
||||
} from './fields/Array.js'
|
||||
|
||||
export type {
|
||||
BlockRowLabelClientComponent,
|
||||
BlockRowLabelServerComponent,
|
||||
BlocksFieldClientComponent,
|
||||
BlocksFieldClientProps,
|
||||
BlocksFieldDescriptionClientComponent,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { BlockRowLabelServerComponent } from 'payload'
|
||||
|
||||
const CustomBlockLabel: BlockRowLabelServerComponent = ({ rowLabel }) => {
|
||||
return <div>{`Custom Block Label: ${rowLabel}`}</div>
|
||||
}
|
||||
|
||||
export default CustomBlockLabel
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user