diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index aa1b860c2c9..6e7d0521861 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -80,12 +80,10 @@ export const MyBlocksField: Field = { The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options: -| Option | Description | -| ---------------------- | -------------------------------------------------------------------------- | -| **`group`** | Text or localization object used to group this Block in the Blocks Drawer. | -| **`initCollapsed`** | Set the initial collapsed state | -| **`isSortable`** | Disable order sorting by setting this value to `false` | -| **`disableBlockName`** | Hide the blockName field by setting this value to `true` | +| Option | Description | +| ------------------- | ------------------------------------------------------ | +| **`initCollapsed`** | Set the initial collapsed state | +| **`isSortable`** | Disable order sorting by setting this value to `false` | #### Customizing the way your block is rendered in Lexical @@ -157,6 +155,19 @@ Blocks are defined as separate configs of their own. | **`dbName`** | Custom table name for this block type when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from slug if not defined. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | +## Block Admin Configs + +In addition to top-level configuration options, each Block in a Blocks field also has configurable admin options. + +| Option | Description | +| ----------------------- | -------------------------------------------------------------------------- | +| **`components.Block`** | Custom component for replacing the Block, including the header. | +| **`components.Label`** | Custom component for replacing the Row Label. | +| **`disableBlockName`** | Hide the blockName field by setting this value to `true`. | +| **`generateBlockName`** | Function to generate a `blockName` to label the block row. | +| **`group`** | Text or localization object used to group this Block in the Blocks Drawer. | +| **`custom`** | Extension point for adding custom data (e.g. for plugins) | + ### Auto-generated data per block In addition to the field data that you define on each block, Payload will store two additional properties on each block: @@ -301,6 +312,8 @@ export const CustomBlocksFieldLabelClient: BlocksFieldLabelClientComponent = ({ ### Row Label +To use a Row Label for your block, first define your Row Label component: + ```tsx 'use client' @@ -315,6 +328,33 @@ export const BlockRowLabel = () => { } ``` +Then add this component to your config Blocks field config to `admin.components.Label` via the [Component Path](/docs/custom-components/overview#component-paths): + +```ts +// ... +{ + type: 'blocks', + name: 'myBlocks', + blocks: [ + { + slug: 'blockWithRowLabel', + admin: { + components: { + Label: '/path/to/my/RowLabel#RowLabel' + } + }, + fields: [ + { + name: 'myText', + type: 'text', + }, + ], + }, + ] +} +// ... +``` + ## Block References If you have multiple blocks used in multiple places, your Payload Config can grow in size, potentially sending more data to the client and requiring more processing on the server. However, you can optimize performance by defining each block **once** in your Payload Config and then referencing its slug wherever it's used instead of passing the entire block config. diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 3050e6250cd..5c0e8e1e0b9 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -42,6 +42,7 @@ import type { CollapsibleFieldLabelClientComponent, CollapsibleFieldLabelServerComponent, ConditionalDateProps, + Data, DateFieldClientProps, DateFieldErrorClientComponent, DateFieldErrorServerComponent, @@ -1406,6 +1407,14 @@ export type Block = { * @default false */ disableBlockName?: boolean + generateBlockName?: (args: { + blockData: Data + data: Data + id?: number | string + operation: 'create' | 'update' + req: PayloadRequest + rowIndex: number + }) => string | undefined group?: Record | string jsx?: PayloadComponent } diff --git a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts index b3896560dc8..e1c8d8b3cc8 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts @@ -467,9 +467,26 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom state[blockNameKey] = {} - if (row.blockName) { - state[blockNameKey].initialValue = row.blockName - state[blockNameKey].value = row.blockName + let blockName: string | undefined = row.blockName + + if ( + !blockName && + block.admin?.generateBlockName && + !block.admin?.disableBlockName + ) { + blockName = block.admin.generateBlockName({ + id, + blockData: blocksValue[i], + data: fullData, + operation, + req, + rowIndex: i, + }) + } + + if (blockName) { + state[blockNameKey].initialValue = blockName + state[blockNameKey].value = blockName } if (includeSchema) { diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index 28f571344b4..130476984c7 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -406,6 +406,55 @@ describe('Block fields', () => { timeout: POLL_TOPASS_TIMEOUT, }) }) + + test('should generated on save when generateBlockName is defined', async () => { + await page.goto(url.create) + await addBlock({ + page, + blockLabel: 'Block With Generated Name', + fieldName: 'blocksWithGeneratedName', + }) + + const fieldInput = page.locator( + '#field-blocksWithGeneratedName .blocks-field__rows #blocksWithGeneratedName-row-0 input[name="blocksWithGeneratedName.0.text"]', + ) + await expect(fieldInput).toBeVisible() + const textToGenerate = 'This is text from my text field' + await fieldInput.fill(textToGenerate) + + await saveDocAndAssert(page) + + const blockNameInput = page.locator( + '#field-blocksWithGeneratedName .section-title input[name="blocksWithGeneratedName.0.blockName"]', + ) + await expect(blockNameInput).toHaveValue(textToGenerate) + }) + + test('should not overwrite existing blockName if present', async () => { + await page.goto(url.create) + await addBlock({ + page, + blockLabel: 'Block With Generated Name', + fieldName: 'blocksWithGeneratedName', + }) + + const fieldInput = page.locator( + '#field-blocksWithGeneratedName .blocks-field__rows #blocksWithGeneratedName-row-0 input[name="blocksWithGeneratedName.0.text"]', + ) + await expect(fieldInput).toBeVisible() + const textToGenerate = 'This is text from my text field' + await fieldInput.fill(textToGenerate) + + const blockNameInput = page.locator( + '#field-blocksWithGeneratedName .section-title input[name="blocksWithGeneratedName.0.blockName"]', + ) + const prefilledBlockNameText = 'This should not be overriden' + await blockNameInput.fill(prefilledBlockNameText) + + await saveDocAndAssert(page) + + await expect(blockNameInput).toHaveValue(prefilledBlockNameText) + }) }) describe('block groups', () => { diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index 491d2d7bc77..fb4465eb357 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -495,6 +495,27 @@ const BlockFields: CollectionConfig = { }, ], }, + { + name: 'blocksWithGeneratedName', + type: 'blocks', + admin: { + description: 'The purpose of this field is to test the admin.generateBlockName function.', + }, + blocks: [ + { + slug: 'blockWithGeneratedName', + admin: { + generateBlockName: ({ blockData }) => blockData.text, + }, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + ], + }, ], }