From 143b6e3b8e789e6b337668df0715c175dd7a0713 Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 5 Mar 2025 18:24:39 +0000 Subject: [PATCH] feat: allow hiding the blockName field visible in blocks' headers via admin.disableBlockName (#11301) Adds a new `admin.disableBlockName` property that allows you to disable the blockName field entirely in the admin view. It defaults to false for backwards compatibility. --- docs/fields/blocks.mdx | 3 +- packages/payload/src/fields/config/client.ts | 9 +++ .../src/fields/config/sanitize.spec.ts | 67 +++++++++++++++++++ packages/payload/src/fields/config/types.ts | 8 ++- packages/ui/src/fields/Blocks/BlockRow.tsx | 4 +- test/fields/collections/Blocks/e2e.spec.ts | 35 ++++++++-- test/fields/collections/Blocks/index.ts | 13 ++++ test/fields/collections/Blocks/shared.ts | 4 ++ test/fields/payload-types.ts | 67 +++++++++++++++++-- 9 files changed, 198 insertions(+), 12 deletions(-) diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index f0299cf0e2..305f604345 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -84,6 +84,7 @@ The Blocks Field inherits all of the default options from the base [Field Admin | **`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` | #### Customizing the way your block is rendered in Lexical @@ -165,7 +166,7 @@ The `blockType` is saved as the slug of the block that has been selected. **`blockName`** -The Admin Panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability. +The Admin Panel provides each block with a `blockName` field which optionally allows editors to label their blocks for better editability and readability. This can be visually hidden via `admin.disableBlockName`. ## Example diff --git a/packages/payload/src/fields/config/client.ts b/packages/payload/src/fields/config/client.ts index 48d6be5733..478a282c99 100644 --- a/packages/payload/src/fields/config/client.ts +++ b/packages/payload/src/fields/config/client.ts @@ -126,6 +126,15 @@ export const createClientBlocks = ({ clientBlock.jsx = jsxResolved } + if (block?.admin?.disableBlockName) { + // Check for existing admin object, this way we don't have to spread it in + if (clientBlock.admin) { + clientBlock.admin.disableBlockName = block.admin.disableBlockName + } else { + clientBlock.admin = { disableBlockName: block.admin.disableBlockName } + } + } + if (block.labels) { clientBlock.labels = {} as unknown as LabelsClient diff --git a/packages/payload/src/fields/config/sanitize.spec.ts b/packages/payload/src/fields/config/sanitize.spec.ts index 5bd1ef17a0..3da7508577 100644 --- a/packages/payload/src/fields/config/sanitize.spec.ts +++ b/packages/payload/src/fields/config/sanitize.spec.ts @@ -362,4 +362,71 @@ describe('sanitizeFields', () => { expect(sanitizedFields).toStrictEqual([]) }) }) + describe('blocks', () => { + it('should maintain admin.blockName true after sanitization', async () => { + const fields: Field[] = [ + { + name: 'noLabelBlock', + type: 'blocks', + blocks: [ + { + slug: 'number', + admin: { + disableBlockName: true, + }, + fields: [ + { + name: 'testNumber', + type: 'number', + }, + ], + }, + ], + label: false, + }, + ] + const sanitizedField = ( + await sanitizeFields({ + config, + fields, + validRelationships: [], + }) + )[0] as BlocksField + + const sanitizedBlock = sanitizedField.blocks[0] + + expect(sanitizedBlock.admin?.disableBlockName).toStrictEqual(true) + }) + it('should default admin.disableBlockName to true after sanitization', async () => { + const fields: Field[] = [ + { + name: 'noLabelBlock', + type: 'blocks', + blocks: [ + { + slug: 'number', + fields: [ + { + name: 'testNumber', + type: 'number', + }, + ], + }, + ], + label: false, + }, + ] + const sanitizedField = ( + await sanitizeFields({ + config, + fields, + validRelationships: [], + }) + )[0] as BlocksField + + const sanitizedBlock = sanitizedField.blocks[0] + + expect(sanitizedBlock.admin?.disableBlockName).toStrictEqual(undefined) + }) + }) }) diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index bcc4fbb6f0..725e9f7293 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -1378,6 +1378,12 @@ export type Block = { } /** Extension point to add your custom data. Available in server and client. */ custom?: Record + /** + * Hides the block name field from the Block's header + * + * @default false + */ + disableBlockName?: boolean group?: Record | string jsx?: PayloadComponent } @@ -1407,7 +1413,7 @@ export type Block = { } export type ClientBlock = { - admin?: Pick + admin?: Pick fields: ClientField[] labels?: LabelsClient } & Pick diff --git a/packages/ui/src/fields/Blocks/BlockRow.tsx b/packages/ui/src/fields/Blocks/BlockRow.tsx index c3bdba4271..f4e867b18e 100644 --- a/packages/ui/src/fields/Blocks/BlockRow.tsx +++ b/packages/ui/src/fields/Blocks/BlockRow.tsx @@ -80,6 +80,8 @@ export const BlockRow: React.FC = ({ const fieldHasErrors = hasSubmitted && errorCount > 0 + const showBlockName = !block.admin?.disableBlockName + const classNames = [ `${baseClass}__row`, fieldHasErrors ? `${baseClass}__row--has-errors` : `${baseClass}__row--no-errors`, @@ -155,7 +157,7 @@ export const BlockRow: React.FC = ({ > {getTranslation(block.labels.singular, i18n)} - + {showBlockName && } {fieldHasErrors && } )} diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index e6a8c8216c..2d223c2661 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -16,7 +16,7 @@ import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js' import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js' import { reInitializeDB } from '../../../helpers/reInitializeDB.js' import { RESTClient } from '../../../helpers/rest.js' -import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js' +import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js' const filename = fileURLToPath(import.meta.url) const currentFolder = path.dirname(filename) @@ -82,7 +82,7 @@ describe('Block fields', () => { const addedRow = page.locator('#field-blocks .blocks-field__row').last() await expect(addedRow).toBeVisible() await expect(addedRow.locator('.blocks-field__block-header')).toHaveText( - 'Custom Block Label: Content 04', + 'Custom Block Label: Content 05', ) }) @@ -156,7 +156,7 @@ describe('Block fields', () => { await duplicateButton.click() const blocks = page.locator('#field-blocks > .blocks-field__rows > div') - expect(await blocks.count()).toEqual(4) + expect(await blocks.count()).toEqual(5) }) test('should save when duplicating subblocks', async () => { @@ -171,7 +171,7 @@ describe('Block fields', () => { await duplicateButton.click() const blocks = page.locator('#field-blocks > .blocks-field__rows > div') - expect(await blocks.count()).toEqual(4) + expect(await blocks.count()).toEqual(5) await page.click('#action-save') await expect(page.locator('.payload-toast-container')).toContainText('successfully') @@ -379,6 +379,33 @@ describe('Block fields', () => { }) }) + describe('blockNames', () => { + test('should show blockName field', async () => { + await page.goto(url.create) + + const blockWithBlockname = page.locator('#field-blocks .blocks-field__rows #blocks-row-1') + + const blocknameField = blockWithBlockname.locator('.section-title') + + await expect(async () => await expect(blocknameField).toBeVisible()).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + + await expect(blocknameField).toHaveAttribute('data-value', 'Second block') + }) + + test("should not show blockName field when it's disabled", async () => { + await page.goto(url.create) + const blockWithBlockname = page.locator('#field-blocks .blocks-field__rows #blocks-row-3') + + await expect( + async () => await expect(blockWithBlockname.locator('.section-title')).toBeHidden(), + ).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) + }) + describe('block groups', () => { test('should render group labels', async () => { await page.goto(url.create) diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index 497f9dc5b6..01879ea566 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -30,6 +30,19 @@ export const getBlocksField = (prefix?: string): BlocksField => ({ }, ], }, + { + slug: prefix ? `${prefix}NoBlockname` : 'noBlockname', + interfaceName: prefix ? `${prefix}NoBlockname` : 'NoBlockname', + admin: { + disableBlockName: true, + }, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, { slug: prefix ? `${prefix}Number` : 'number', interfaceName: prefix ? `${prefix}NumberBlock` : 'NumberBlock', diff --git a/test/fields/collections/Blocks/shared.ts b/test/fields/collections/Blocks/shared.ts index 6087295831..b1326d829c 100644 --- a/test/fields/collections/Blocks/shared.ts +++ b/test/fields/collections/Blocks/shared.ts @@ -32,6 +32,10 @@ export const getBlocksFieldSeedData = (prefix?: string): any => [ }, ], }, + { + blockType: prefix ? `${prefix}NoBlockname` : 'noBlockname', + text: 'Hello world', + }, ] export const blocksDoc: Partial = { diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 17c2df7354..ccf48f98ad 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -699,16 +699,29 @@ export interface ArrayField { */ export interface BlockField { id: string; - blocks: (ContentBlock | NumberBlock | SubBlocksBlock | TabsBlock)[]; - duplicate: (ContentBlock | NumberBlock | SubBlocksBlock | TabsBlock)[]; + blocks: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[]; + duplicate: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[]; collapsedByDefaultBlocks: ( | LocalizedContentBlock + | LocalizedNoBlockname + | LocalizedNumberBlock + | LocalizedSubBlocksBlock + | LocalizedTabsBlock + )[]; + disableSort: ( + | LocalizedContentBlock + | LocalizedNoBlockname + | LocalizedNumberBlock + | LocalizedSubBlocksBlock + | LocalizedTabsBlock + )[]; + localizedBlocks: ( + | LocalizedContentBlock + | LocalizedNoBlockname | LocalizedNumberBlock | LocalizedSubBlocksBlock | LocalizedTabsBlock )[]; - disableSort: (LocalizedContentBlock | LocalizedNumberBlock | LocalizedSubBlocksBlock | LocalizedTabsBlock)[]; - localizedBlocks: (LocalizedContentBlock | LocalizedNumberBlock | LocalizedSubBlocksBlock | LocalizedTabsBlock)[]; i18nBlocks?: | { text?: string | null; @@ -883,6 +896,16 @@ export interface ContentBlock { blockName?: string | null; blockType: 'content'; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "NoBlockname". + */ +export interface NoBlockname { + text: string; + id?: string | null; + blockName?: string | null; + blockType: 'noBlockname'; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "NumberBlock". @@ -939,6 +962,16 @@ export interface LocalizedContentBlock { blockName?: string | null; blockType: 'localizedContent'; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedNoBlockname". + */ +export interface LocalizedNoBlockname { + text: string; + id?: string | null; + blockName?: string | null; + blockType: 'localizedNoBlockname'; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localizedNumberBlock". @@ -1801,7 +1834,7 @@ export interface TabsField { text: string; id?: string | null; }[]; - blocks: (ContentBlock | NumberBlock | SubBlocksBlock | TabsBlock)[]; + blocks: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[]; group: { number: number; }; @@ -2461,6 +2494,7 @@ export interface BlockFieldsSelect { | T | { content?: T | ContentBlockSelect; + noBlockname?: T | NoBlocknameSelect; number?: T | NumberBlockSelect; subBlocks?: T | SubBlocksBlockSelect; tabs?: T | TabsBlockSelect; @@ -2469,6 +2503,7 @@ export interface BlockFieldsSelect { | T | { content?: T | ContentBlockSelect; + noBlockname?: T | NoBlocknameSelect; number?: T | NumberBlockSelect; subBlocks?: T | SubBlocksBlockSelect; tabs?: T | TabsBlockSelect; @@ -2477,6 +2512,7 @@ export interface BlockFieldsSelect { | T | { localizedContent?: T | LocalizedContentBlockSelect; + localizedNoBlockname?: T | LocalizedNoBlocknameSelect; localizedNumber?: T | LocalizedNumberBlockSelect; localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; localizedTabs?: T | LocalizedTabsBlockSelect; @@ -2485,6 +2521,7 @@ export interface BlockFieldsSelect { | T | { localizedContent?: T | LocalizedContentBlockSelect; + localizedNoBlockname?: T | LocalizedNoBlocknameSelect; localizedNumber?: T | LocalizedNumberBlockSelect; localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; localizedTabs?: T | LocalizedTabsBlockSelect; @@ -2493,6 +2530,7 @@ export interface BlockFieldsSelect { | T | { localizedContent?: T | LocalizedContentBlockSelect; + localizedNoBlockname?: T | LocalizedNoBlocknameSelect; localizedNumber?: T | LocalizedNumberBlockSelect; localizedSubBlocks?: T | LocalizedSubBlocksBlockSelect; localizedTabs?: T | LocalizedTabsBlockSelect; @@ -2690,6 +2728,15 @@ export interface ContentBlockSelect { id?: T; blockName?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "NoBlockname_select". + */ +export interface NoBlocknameSelect { + text?: T; + id?: T; + blockName?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "NumberBlock_select". @@ -2739,6 +2786,15 @@ export interface LocalizedContentBlockSelect { id?: T; blockName?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "localizedNoBlockname_select". + */ +export interface LocalizedNoBlocknameSelect { + text?: T; + id?: T; + blockName?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "localizedNumberBlock_select". @@ -3404,6 +3460,7 @@ export interface TabsFieldsSelect { | T | { content?: T | ContentBlockSelect; + noBlockname?: T | NoBlocknameSelect; number?: T | NumberBlockSelect; subBlocks?: T | SubBlocksBlockSelect; tabs?: T | TabsBlockSelect;