From 22f61ad79ec780602338fcbf091624f259e65f07 Mon Sep 17 00:00:00 2001 From: Said Akhrarov <36972061+akhrarovsaid@users.noreply.github.com> Date: Thu, 20 Feb 2025 14:51:47 -0500 Subject: [PATCH] feat(ui): adds support for block groups (#11239) ### What? This PR introduces support for the `admin.group` property in block configs. This property enables blocks to be grouped under a common, potentially localized, label in the block drawer component. This makes it easier to sort through large collections of blocks. Previously, all blocks would be in one common layout. This PR also encompasses documentation changes and e2e tests to check for the rendering of group labels. ### Why? To make it easier to organize many blocks in block fields. ### How? By introducing a new `admin.group` property in block configs and assembling them in the blocks drawer component. Before: ![Editing-Block-Field-Payload--before](https://github.com/user-attachments/assets/fb0c887b-ee47-46a1-a249-c4a4b7a5c13c) After: ![Editing-Block-Field-Payload--after](https://github.com/user-attachments/assets/046d5a6f-3108-4464-ac69-8b7afcf27094) Demo: [Editing---Block-Field---Payload-groups-demo.webm](https://github.com/user-attachments/assets/2b351dc1-0d14-4a5b-ae71-bcd31fbb23df) Addresses #5609 --- docs/fields/blocks.mdx | 9 +- packages/payload/src/fields/config/client.ts | 10 ++- packages/payload/src/fields/config/types.ts | 3 +- .../src/fields/Blocks/BlocksDrawer/index.scss | 43 +++++++++ .../src/fields/Blocks/BlocksDrawer/index.tsx | 90 +++++++++++++------ test/fields/collections/Blocks/e2e.spec.ts | 28 ++++++ test/fields/collections/Blocks/index.ts | 57 ++++++++++++ test/fields/payload-types.ts | 63 +++++++++++++ 8 files changed, 270 insertions(+), 33 deletions(-) diff --git a/docs/fields/blocks.mdx b/docs/fields/blocks.mdx index 397ba6fcf..f0299cf0e 100644 --- a/docs/fields/blocks.mdx +++ b/docs/fields/blocks.mdx @@ -79,10 +79,11 @@ 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 | -| ------------------- | ---------------------------------- | -| **`initCollapsed`** | Set the initial collapsed state | -| **`isSortable`** | Disable order sorting by setting this value to `false` | +| 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` | #### Customizing the way your block is rendered in Lexical diff --git a/packages/payload/src/fields/config/client.ts b/packages/payload/src/fields/config/client.ts index 8dfa46d76..48d6be573 100644 --- a/packages/payload/src/fields/config/client.ts +++ b/packages/payload/src/fields/config/client.ts @@ -107,9 +107,13 @@ export const createClientBlocks = ({ clientBlock.imageURL = block.imageURL } - if (block.admin?.custom) { - clientBlock.admin = { - custom: block.admin.custom, + if (block.admin?.custom || block.admin?.group) { + clientBlock.admin = {} + if (block.admin.custom) { + clientBlock.admin.custom = block.admin.custom + } + if (block.admin.group) { + clientBlock.admin.group = block.admin.group } } diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index af914d1b1..c14a96db2 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -1373,6 +1373,7 @@ export type Block = { } /** Extension point to add your custom data. Available in server and client. */ custom?: Record + group?: Record | string jsx?: PayloadComponent } /** Extension point to add your custom data. Server only. */ @@ -1401,7 +1402,7 @@ export type Block = { } export type ClientBlock = { - admin?: Pick + admin?: Pick fields: ClientField[] labels?: LabelsClient } & Pick diff --git a/packages/ui/src/fields/Blocks/BlocksDrawer/index.scss b/packages/ui/src/fields/Blocks/BlocksDrawer/index.scss index efe5c5fc9..07d094772 100644 --- a/packages/ui/src/fields/Blocks/BlocksDrawer/index.scss +++ b/packages/ui/src/fields/Blocks/BlocksDrawer/index.scss @@ -21,6 +21,32 @@ padding-top: base(0.75); } + &__block-groups { + padding: 0; + display: flex; + flex-direction: column; + gap: base(1.5); + } + + &__block-group { + list-style: none; + } + + &__block-group-label { + padding-bottom: base(0.5); + } + + &__block-group-none { + order: 1; + padding-top: base(1.5); + border-top: 1px solid var(--theme-border-color); + + &:only-child { + padding-top: 0; + border-top: 0; + } + } + @include large-break { &__blocks { grid-template-columns: repeat(5, 1fr); @@ -31,10 +57,19 @@ &__blocks-wrapper { padding-top: base(1.75); } + &__blocks { grid-template-columns: repeat(3, 1fr); gap: base(0.5); } + + &__block-groups { + gap: base(1.75); + } + + &__block-group-none { + padding-top: base(1.75); + } } @include small-break { @@ -45,6 +80,14 @@ &__blocks { grid-template-columns: repeat(2, 1fr); } + + &__block-groups { + gap: base(0.75); + } + + &__block-group-none { + padding-top: base(0.75); + } } } } diff --git a/packages/ui/src/fields/Blocks/BlocksDrawer/index.tsx b/packages/ui/src/fields/Blocks/BlocksDrawer/index.tsx index 041427081..4b638a066 100644 --- a/packages/ui/src/fields/Blocks/BlocksDrawer/index.tsx +++ b/packages/ui/src/fields/Blocks/BlocksDrawer/index.tsx @@ -4,7 +4,7 @@ import type { ClientBlock, Labels } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Drawer } from '../../../elements/Drawer/index.js' import { ThumbnailCard } from '../../../elements/ThumbnailCard/index.js' @@ -43,6 +43,27 @@ export const BlocksDrawer: React.FC = (props) => { const { i18n, t } = useTranslation() const { config } = useConfig() + const blockGroups = useMemo(() => { + const groups: Record = { + _none: [], + } + filteredBlocks.forEach((block) => { + if (typeof block === 'object' && block.admin?.group) { + const group = block.admin.group + const label = typeof group === 'string' ? group : getTranslation(group, i18n) + + if (Object.hasOwn(groups, label)) { + groups[label].push(block) + } else { + groups[label] = [block] + } + } else { + groups._none.push(block) + } + }) + return groups + }, [filteredBlocks, i18n]) + useEffect(() => { if (!isModalOpen(drawerSlug)) { setSearchTerm('') @@ -71,34 +92,53 @@ export const BlocksDrawer: React.FC = (props) => { >
-
    - {filteredBlocks?.map((_block, index) => { - const block = typeof _block === 'string' ? config.blocksMap[_block] : _block +
      + {Object.entries(blockGroups).map(([groupLabel, groupBlocks]) => + !groupBlocks.length ? null : ( +
    • + {groupLabel !== '_none' && ( +

      {groupLabel}

      + )} +
        + {groupBlocks.map((_block, index) => { + const block = typeof _block === 'string' ? config.blocksMap[_block] : _block - const { slug, imageAltText, imageURL, labels: blockLabels } = block + const { slug, imageAltText, imageURL, labels: blockLabels } = block - return ( -
      • - { - void addRow(addRowIndex, slug) - closeModal(drawerSlug) - }} - thumbnail={ - imageURL ? ( - {imageAltText} - ) : ( -
        - -
        + return ( +
      • + { + void addRow(addRowIndex, slug) + closeModal(drawerSlug) + }} + thumbnail={ + imageURL ? ( + {imageAltText} + ) : ( +
        + +
        + ) + } + /> +
      • ) - } - /> + })} +
    • - ) - })} + ), + )}
diff --git a/test/fields/collections/Blocks/e2e.spec.ts b/test/fields/collections/Blocks/e2e.spec.ts index d012727a8..ef4a63cf4 100644 --- a/test/fields/collections/Blocks/e2e.spec.ts +++ b/test/fields/collections/Blocks/e2e.spec.ts @@ -344,4 +344,32 @@ describe('Block fields', () => { expect(await field.count()).toEqual(0) }) }) + + describe('block groups', () => { + test('should render group labels', async () => { + await page.goto(url.create) + const addButton = page.locator('#field-groupedBlocks > .blocks-field__drawer-toggler') + await addButton.click() + + const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]') + await expect(blocksDrawer).toBeVisible() + + const groupLabel = blocksDrawer.locator('.blocks-drawer__block-group-label').first() + await expect(groupLabel).toBeVisible() + await expect(groupLabel).toHaveText('Group') + }) + + test('should render localized group labels', async () => { + await page.goto(url.create) + const addButton = page.locator('#field-groupedBlocks > .blocks-field__drawer-toggler') + await addButton.click() + + const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]') + await expect(blocksDrawer).toBeVisible() + + const groupLabel = blocksDrawer.locator('.blocks-drawer__block-group-label').nth(1) + await expect(groupLabel).toBeVisible() + await expect(groupLabel).toHaveText('Group in en') + }) + }) }) diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index db93506f2..497f9dc5b 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -424,6 +424,63 @@ const BlockFields: CollectionConfig = { blockReferences: ['localizedTextReference2'], blocks: [], }, + { + name: 'groupedBlocks', + type: 'blocks', + admin: { + description: 'The purpose of this field is to test Block groups.', + }, + blocks: [ + { + slug: 'blockWithGroupOne', + admin: { + group: 'Group', + }, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + { + slug: 'blockWithGroupTwo', + admin: { + group: 'Group', + }, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + { + slug: 'blockWithLocalizedGroup', + admin: { + group: { + en: 'Group in en', + es: 'Group in es', + }, + }, + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + { + slug: 'blockWithoutGroup', + fields: [ + { + name: 'text', + type: 'text', + }, + ], + }, + ], + }, ], } diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 065c473c8..61d47b8f7 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -834,6 +834,37 @@ export interface BlockField { deduplicatedBlocks2?: ConfigBlockTest[] | null; localizedReferencesLocalizedBlock?: LocalizedTextReference[] | null; localizedReferences?: LocalizedTextReference2[] | null; + /** + * The purpose of this field is to test Block groups. + */ + groupedBlocks?: + | ( + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithGroupOne'; + } + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithGroupTwo'; + } + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithLocalizedGroup'; + } + | { + text?: string | null; + id?: string | null; + blockName?: string | null; + blockType: 'blockWithoutGroup'; + } + )[] + | null; updatedAt: string; createdAt: string; } @@ -2587,6 +2618,38 @@ export interface BlockFieldsSelect { deduplicatedBlocks2?: T | {}; localizedReferencesLocalizedBlock?: T | {}; localizedReferences?: T | {}; + groupedBlocks?: + | T + | { + blockWithGroupOne?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + blockWithGroupTwo?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + blockWithLocalizedGroup?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + blockWithoutGroup?: + | T + | { + text?: T; + id?: T; + blockName?: T; + }; + }; updatedAt?: T; createdAt?: T; }