Skip to content

feat: add support for generating block names #12135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions docs/fields/blocks.mdx
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
@@ -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, string> | string
jsx?: PayloadComponent
}
Original file line number Diff line number Diff line change
@@ -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) {
49 changes: 49 additions & 0 deletions test/fields/collections/Blocks/e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
21 changes: 21 additions & 0 deletions test/fields/collections/Blocks/index.ts
Original file line number Diff line number Diff line change
@@ -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',
},
],
},
],
},
],
}

Loading