Skip to content

Commit

Permalink
Custom block categories (#1504)
Browse files Browse the repository at this point in the history
Allows specifying custom block categories in application code.
  • Loading branch information
johnnyomair authored Dec 14, 2023
1 parent 0ff9b9b commit a227388
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 53 deletions.
37 changes: 37 additions & 0 deletions .changeset/two-carpets-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"@comet/blocks-admin": minor
---

Add support for custom block categories

Allows specifying custom block categories in application code.

**Example:**

In `src/common/blocks/customBlockCategories.tsx`:

```tsx
import { BlockCategory, CustomBlockCategory } from "@comet/blocks-admin";
import React from "react";
import { FormattedMessage } from "react-intl";

const productsBlockCategory: CustomBlockCategory = {
id: "Products",
label: <FormattedMessage id="blocks.category.products" defaultMessage="Products" />,
// Specify where category will be shown in drawer
insertBefore: BlockCategory.Teaser,
};

export { productsBlockCategory };
```

In `src/documents/pages/blocks/MyBlock.tsx`:

```tsx
import { productsBlockCategory } from "@src/common/blocks/customBlockCategories";

const MyBlock: BlockInterface = {
category: productsBlockCategory,
...
};
```
11 changes: 11 additions & 0 deletions demo/admin/src/common/blocks/customBlockCategories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BlockCategory } from "@comet/blocks-admin";
import React from "react";
import { FormattedMessage } from "react-intl";

const customBlockCategory = {
id: "Custom",
label: <FormattedMessage id="blocks.category.custom" defaultMessage="Custom" />,
insertBefore: BlockCategory.Media,
};

export { customBlockCategory };
36 changes: 21 additions & 15 deletions demo/admin/src/pages/blocks/FullWidthImageBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { messages } from "@comet/admin";
import { BlockCategory, createCompositeBlock, createOptionalBlock } from "@comet/blocks-admin";
import { createCompositeBlock, createOptionalBlock } from "@comet/blocks-admin";
import { DamImageBlock } from "@comet/cms-admin";
import { customBlockCategory } from "@src/common/blocks/customBlockCategories";
import { RichTextBlock } from "@src/common/blocks/RichTextBlock";
import * as React from "react";
import { FormattedMessage } from "react-intl";
Expand All @@ -9,19 +10,24 @@ const FullWidthImageContentBlock = createOptionalBlock(RichTextBlock, {
title: <FormattedMessage {...messages.content} />,
});

export const FullWidthImageBlock = createCompositeBlock({
name: "FullWidthImage",
displayName: <FormattedMessage id="blocks.fullWidthImage" defaultMessage="Full Width Image" />,
category: BlockCategory.Media,
blocks: {
image: {
block: DamImageBlock,
title: <FormattedMessage {...messages.image} />,
paper: true,
},
content: {
block: FullWidthImageContentBlock,
title: <FormattedMessage {...messages.content} />,
export const FullWidthImageBlock = createCompositeBlock(
{
name: "FullWidthImage",
displayName: <FormattedMessage id="blocks.fullWidthImage" defaultMessage="Full Width Image" />,
blocks: {
image: {
block: DamImageBlock,
title: <FormattedMessage {...messages.image} />,
paper: true,
},
content: {
block: FullWidthImageContentBlock,
title: <FormattedMessage {...messages.content} />,
},
},
},
});
(block) => {
block.category = customBlockCategory;
return block;
},
);
31 changes: 19 additions & 12 deletions demo/admin/src/pages/blocks/TwoListsBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { createCompositeBlock, createListBlock } from "@comet/blocks-admin";
import { customBlockCategory } from "@src/common/blocks/customBlockCategories";
import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock";

const TwoListsListBlock = createListBlock({
name: "TwoListsList",
block: HeadlineBlock,
});

export const TwoListsBlock = createCompositeBlock({
name: "TwoLists",
displayName: "Two Lists",
blocks: {
list1: {
block: TwoListsListBlock,
title: "List 1",
},
list2: {
block: TwoListsListBlock,
title: "List 2",
export const TwoListsBlock = createCompositeBlock(
{
name: "TwoLists",
displayName: "Two Lists",
blocks: {
list1: {
block: TwoListsListBlock,
title: "List 1",
},
list2: {
block: TwoListsListBlock,
title: "List 2",
},
},
},
});
(block) => {
block.category = customBlockCategory;
return block;
},
);
93 changes: 75 additions & 18 deletions packages/admin/blocks-admin/src/blocks/common/AddBlockDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import { styled } from "@mui/material/styles";
import * as React from "react";
import { FormattedMessage, MessageDescriptor, useIntl } from "react-intl";

import { BlockCategory, blockCategoryLabels, BlockInterface } from "../types";
import { BlockCategory, blockCategoryLabels, BlockInterface, CustomBlockCategory } from "../types";

type BlockType = string;

interface Category {
blockCategory: BlockCategory;
id: string;
label: React.ReactNode;
blocks: Array<[BlockType, BlockInterface]>;
}
Expand All @@ -38,27 +38,76 @@ interface Props {

export function AddBlockDrawer({ open, onClose, blocks, onAddNewBlock }: Props): React.ReactElement {
const intl = useIntl();
const [categories, setCategories] = React.useState<Category[]>([]);
const [searchValue, setSearchValue] = React.useState("");
const [addAndEdit, setAddAndEdit] = useStoredState<boolean>("addAndEdit", true);

React.useEffect(() => {
setCategories(
(Object.keys(BlockCategory) as BlockCategory[]).map((currentBlockCategory) => {
const blocksForCategory = Object.entries(blocks).filter(([, block]) => {
const formattedDisplayName =
typeof block.displayName === "string"
? (block.displayName as string)
: intl.formatMessage((block.displayName as React.ReactElement<MessageDescriptor>).props);
const categories = React.useMemo(() => {
const categories: Category[] = [];
const categoriesOrder = Object.keys(BlockCategory);

return (
block.category === currentBlockCategory && formattedDisplayName.toLocaleLowerCase().includes(searchValue.toLocaleLowerCase())
for (const [type, block] of Object.entries(blocks)) {
let blockName: string;

if (typeof block.displayName === "string") {
blockName = block.displayName;
} else if (isFormattedMessage(block.displayName)) {
blockName = intl.formatMessage(block.displayName.props);
} else {
throw new TypeError("Block displayName must be either a string or a FormattedMessage");
}

if (!blockName.toLocaleLowerCase().includes(searchValue.toLocaleLowerCase())) {
continue;
}

let id: string;
let label: React.ReactNode;

if (isCustomBlockCategory(block.category)) {
if (block.category.id in BlockCategory) {
throw new Error(
`Custom block category "${block.category.id}" cannot override default block category BlockCategory.${block.category.id}`,
);
});
}

id = block.category.id;
label = block.category.label;

if (block.category.insertBefore) {
const insertBeforeIndex = categoriesOrder.indexOf(block.category.insertBefore);

if (categoriesOrder.includes(id)) {
const hasDifferentInsertBefore = insertBeforeIndex - 1 !== categoriesOrder.indexOf(id);

if (hasDifferentInsertBefore) {
throw new Error(`Custom block category "${id}" has different "insertBefore" values`);
}
} else {
categoriesOrder.splice(insertBeforeIndex, 0, id);
}
} else {
if (!categoriesOrder.includes(id)) {
categoriesOrder.push(id);
}
}
} else {
id = block.category;
label = blockCategoryLabels[block.category];
}

let category = categories.find((category) => category.id === id);

if (!category) {
category = { id, label, blocks: [] };
categories.push(category);
}

category.blocks.push([type, block]);
}

return { blockCategory: currentBlockCategory, label: blockCategoryLabels[currentBlockCategory], blocks: blocksForCategory };
}),
);
categories.sort((a, b) => categoriesOrder.indexOf(a.id) - categoriesOrder.indexOf(b.id));

return categories;
}, [intl, blocks, searchValue]);

const handleListItemClick = (type: string) => {
Expand Down Expand Up @@ -122,7 +171,7 @@ export function AddBlockDrawer({ open, onClose, blocks, onAddNewBlock }: Props):
}

return (
<ContentItem key={category.blockCategory}>
<ContentItem key={category.id}>
<Typography variant="h4" gutterBottom>
{category.label}
</Typography>
Expand All @@ -146,6 +195,14 @@ export function AddBlockDrawer({ open, onClose, blocks, onAddNewBlock }: Props):
);
}

function isCustomBlockCategory(category: BlockCategory | CustomBlockCategory): category is CustomBlockCategory {
return typeof category === "object";
}

function isFormattedMessage(node: React.ReactNode): node is React.ReactElement<MessageDescriptor> {
return React.isValidElement(node) && node.type === FormattedMessage;
}

const Content = styled(DialogContent)`
width: 380px;
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { AdminComponentStickyFooter } from "../common/AdminComponentStickyFooter
import { BlockRow } from "../common/blockRow/BlockRow";
import { createBlockSkeleton } from "../helpers/createBlockSkeleton";
import { deduplicateBlockDependencies } from "../helpers/deduplicateBlockDependencies";
import { BlockCategory, BlockDependency, BlockInputApi, BlockInterface, DispatchSetStateAction, PreviewContent } from "../types";
import { BlockCategory, BlockDependency, BlockInputApi, BlockInterface, CustomBlockCategory, DispatchSetStateAction, PreviewContent } from "../types";
import { resolveNewState } from "../utils";
import { FinalFormColumnsSelect } from "./columnsBlock/FinalFormColumnsSelect";
import { FinalFormLayoutSelect } from "./columnsBlock/FinalFormLayoutSelect";
Expand Down Expand Up @@ -54,7 +54,7 @@ interface ColumnsBlockState<T extends BlockInterface> {
interface CreateColumnsBlockOptions<T extends BlockInterface> {
name: string;
displayName: React.ReactNode;
category?: BlockCategory;
category?: BlockCategory | CustomBlockCategory;
contentBlock: T;
layouts: ColumnsBlockLayout[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { BlockInterfaceWithOptions } from "../helpers/composeBlocks/types";
import { normalizedBlockConfig } from "../helpers/composeBlocks/utils";
import { createBlockSkeleton } from "../helpers/createBlockSkeleton";
import { isBlockInterface } from "../helpers/isBlockInterface";
import { BlockCategory, BlockInputApi, BlockInterface, BlockOutputApi, BlockState } from "../types";
import { BlockCategory, BlockInputApi, BlockInterface, BlockOutputApi, BlockState, CustomBlockCategory } from "../types";

interface BlockConfiguration {
title?: React.ReactNode;
Expand All @@ -41,7 +41,7 @@ interface CreateCompositeBlockOptionsBase {
/**
* @deprecated Use override instead to adapt the factored block
*/
category?: BlockCategory;
category?: BlockCategory | CustomBlockCategory;
adminLayout?: "stacked";
blocks: Record<string, BlockConfiguration>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { parallelAsyncEvery } from "../../utils/parallelAsyncEvery";
import { useAdminComponentPaper } from "../common/AdminComponentPaper";
import { HiddenInSubroute } from "../common/HiddenInSubroute";
import { createBlockSkeleton } from "../helpers/createBlockSkeleton";
import { BlockCategory, BlockInterface, BlockState, DispatchSetStateAction, PreviewStateInterface } from "../types";
import { BlockCategory, BlockInterface, BlockState, CustomBlockCategory, DispatchSetStateAction, PreviewStateInterface } from "../types";
import { resolveNewState } from "../utils";

interface OneOfBlockItem<T extends BlockInterface = BlockInterface> {
Expand Down Expand Up @@ -57,7 +57,7 @@ export interface CreateOneOfBlockOptions<T extends boolean> {
name: string;
displayName?: React.ReactNode;
supportedBlocks: Record<BlockType, BlockInterface>;
category?: BlockCategory;
category?: BlockCategory | CustomBlockCategory;
variant?: "select" | "radio" | "toggle";
allowEmpty?: T;
}
Expand Down
6 changes: 4 additions & 2 deletions packages/admin/blocks-admin/src/blocks/types.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { FormattedMessage } from "react-intl";
import { FormattedMessage, MessageDescriptor } from "react-intl";

export type SetStateFn<S> = (prevState: S) => S;
export type SetStateAction<S> = S | SetStateFn<S>;
Expand Down Expand Up @@ -110,7 +110,7 @@ export interface BlockInterface<
> extends AnonymousBlockInterface<InputApi, State, OutputApi, PreviewState> {
name: string;
displayName: React.ReactNode;
category: BlockCategory;
category: BlockCategory | CustomBlockCategory;
}

export interface RootBlockInterface<
Expand Down Expand Up @@ -150,6 +150,8 @@ export enum BlockCategory {
Other = "Other",
}

export type CustomBlockCategory = { id: string; label: string | React.ReactElement<MessageDescriptor>; insertBefore?: BlockCategory };

export const blockCategoryLabels = {
[BlockCategory.TextAndContent]: <FormattedMessage id="comet.blocks.category.textAndContent" defaultMessage="Text & Content" />,
[BlockCategory.Media]: <FormattedMessage id="comet.blocks.category.media" defaultMessage="Media" />,
Expand Down
1 change: 1 addition & 0 deletions packages/admin/blocks-admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type {
SetStateAction,
SetStateFn,
} from "./blocks/types";
export type { CustomBlockCategory } from "./blocks/types";
export { BlockCategory, blockCategoryLabels } from "./blocks/types";
export { resolveNewState } from "./blocks/utils";
export { YouTubeVideoBlock } from "./blocks/YouTubeVideoBlock";
Expand Down

0 comments on commit a227388

Please sign in to comment.