Skip to content
Open
Show file tree
Hide file tree
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ Most common block types are supported. We happily accept pull requests to add su
| Checkbox | ❌ Missing | Supported by [react-notion-x](https://github.com/NotionX/react-notion-x) |
| Table Of Contents | ❌ Missing | Supported by [react-notion-x](https://github.com/NotionX/react-notion-x) |

## Unsupported Blocks

Unsupported blocks render a visible warning by default instead of failing silently.

```jsx
<NotionRenderer blockMap={blockMap} />
```

To keep the previous empty-placeholder behavior, disable warnings:

```jsx
<NotionRenderer blockMap={blockMap} showUnsupportedBlockErrors={false} />
```

You can also customize the fallback:

```jsx
<NotionRenderer
blockMap={blockMap}
renderUnsupportedBlock={block => (
<div>Unsupported block: {block.value.type}</div>
)}
/>
```

## Block Type Specific Caveats

When using a code block in your Notion page, `NotionRenderer` will use `prismjs` to detect the language of the code block.
Expand Down
41 changes: 33 additions & 8 deletions src/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
MapPageUrl,
MapImageUrl,
CustomBlockComponents,
BlockValueProp,
CustomDecoratorComponents,
CustomDecoratorComponentProps
} from "./types";
import Asset from "./components/asset";
import Code from "./components/code";
import PageIcon from "./components/page-icon";
import PageHeader from "./components/page-header";
import UnsupportedBlock from "./components/unsupported-block";
import { classNames, getTextContent, getListNumber } from "./utils";

export const createRenderChildText = (
Expand Down Expand Up @@ -94,6 +94,8 @@ interface Block {
hideHeader?: boolean;
customBlockComponents?: CustomBlockComponents;
customDecoratorComponents?: CustomDecoratorComponents;
showUnsupportedBlockErrors?: boolean;
renderUnsupportedBlock?: (block: BlockType) => React.ReactNode;
}

export const Block: React.FC<Block> = props => {
Expand All @@ -107,13 +109,36 @@ export const Block: React.FC<Block> = props => {
mapPageUrl,
mapImageUrl,
customBlockComponents,
customDecoratorComponents
customDecoratorComponents,
showUnsupportedBlockErrors = true,
renderUnsupportedBlock
} = props;
const blockValue = block?.value;

const renderComponent = () => {
const renderChildText = createRenderChildText(customDecoratorComponents);

const renderUnsupported = () => {
if (process.env.NODE_ENV !== "production") {
console.warn("Unsupported type " + block?.value?.type);
}

if (!showUnsupportedBlockErrors) {
return <div />;
}

if (renderUnsupportedBlock) {
return <>{renderUnsupportedBlock(block)}</>;
}

return (
<UnsupportedBlock
blockType={block?.value?.type}
blockId={block?.value?.id}
/>
);
};

switch (blockValue?.type) {
case "page":
if (level === 0) {
Expand Down Expand Up @@ -361,6 +386,7 @@ export const Block: React.FC<Block> = props => {
);
case "collection_view":
if (!block) return null;
if (!block.collection) return renderUnsupported();

const collectionView = block?.collection?.types[0];

Expand Down Expand Up @@ -516,10 +542,7 @@ export const Block: React.FC<Block> = props => {
</details>
);
default:
if (process.env.NODE_ENV !== "production") {
console.log("Unsupported type " + block?.value?.type);
}
return <div />;
return renderUnsupported();
}
return null;
};
Expand All @@ -531,12 +554,14 @@ export const Block: React.FC<Block> = props => {
// Do not use custom component for base page block
level !== 0
) {
const CustomComponent = customBlockComponents[blockValue?.type]!;
const CustomComponent = customBlockComponents[blockValue?.type] as React.FC<
any
>;
return (
<CustomComponent
renderComponent={renderComponent}
blockMap={blockMap}
blockValue={blockValue as BlockValueProp<typeof blockValue.type>}
blockValue={blockValue}
level={level}
>
{children}
Expand Down
46 changes: 46 additions & 0 deletions src/components/unsupported-block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from "react";

interface UnsupportedBlockProps {
blockType?: string;
blockId?: string;
}

const unsupportedBlockLabels: { [blockType: string]: string } = {
checkbox: "Checkbox",
collection_view: "Database",
collection_view_page: "Database",
database: "Database",
equation: "Equation",
synced_block: "Synced Block",
table_of_contents: "Table of Contents"
};

export const getUnsupportedBlockMessage = (blockType?: string) => {
const label =
unsupportedBlockLabels[blockType || ""] || blockType || "Unsupported";
const article = /^[aeiou]/i.test(label) ? "an" : "a";

return `This Notion document includes ${article} ${label} block which react-notion cannot render. Please remove it to render this page.`;
};

const UnsupportedBlock: React.FC<UnsupportedBlockProps> = ({
blockType,
blockId
}) => (
<div className="notion-unsupported-block" role="alert">
<div className="notion-unsupported-block-icon" aria-hidden="true">
!
</div>
<div>
<div className="notion-unsupported-block-title">
Unsupported Notion block
</div>
<div>{getUnsupportedBlockMessage(blockType)}</div>
{blockId && (
<div className="notion-unsupported-block-id">Block ID: {blockId}</div>
)}
</div>
</div>
);

export default UnsupportedBlock;
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { NotionRenderer } from "./renderer";
export { default as UnsupportedBlock } from "./components/unsupported-block";
export { getUnsupportedBlockMessage } from "./components/unsupported-block";
export * from "./types";
export * from "./utils";
export * from "./block";
5 changes: 4 additions & 1 deletion src/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
MapPageUrl,
MapImageUrl,
CustomBlockComponents,
CustomDecoratorComponents
CustomDecoratorComponents,
BlockType
} from "./types";
import { Block } from "./block";
import { defaultMapImageUrl, defaultMapPageUrl } from "./utils";
Expand All @@ -20,6 +21,8 @@ export interface NotionRendererProps {
level?: number;
customBlockComponents?: CustomBlockComponents;
customDecoratorComponents?: CustomDecoratorComponents;
showUnsupportedBlockErrors?: boolean;
renderUnsupportedBlock?: (block: BlockType) => React.ReactNode;
}

export const NotionRenderer: React.FC<NotionRendererProps> = ({
Expand Down
40 changes: 40 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -681,3 +681,43 @@ img.notion-nav-icon {
margin: 0 2px;
color: rgba(55, 53, 47, 0.4);
}

.notion-unsupported-block {
display: flex;
gap: 10px;
align-items: flex-start;
margin: 8px 0;
padding: 12px;
border: 1px solid rgba(235, 87, 87, 0.35);
border-radius: 4px;
background: rgba(235, 87, 87, 0.08);
color: rgb(55, 53, 47);
}

.notion-unsupported-block-icon {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-top: 2px;
border-radius: 50%;
background: rgb(235, 87, 87);
color: white;
font-size: 12px;
font-weight: 700;
line-height: 1;
}

.notion-unsupported-block-title {
margin-bottom: 2px;
font-weight: 600;
}

.notion-unsupported-block-id {
margin-top: 6px;
color: rgba(55, 53, 47, 0.65);
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
font-size: 12px;
}
125 changes: 125 additions & 0 deletions test/unsupported-block.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import * as React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { NotionRenderer } from "../src/renderer";
import { BlockMapType } from "../src/types";

const baseValue = (id: string, type: string, parentId = "page") => ({
id,
version: 1,
type,
parent_id: parentId,
parent_table: "block",
alive: true,
created_time: 1,
last_edited_time: 1,
created_by_table: "notion_user",
created_by_id: "user",
last_edited_by_table: "notion_user",
last_edited_by_id: "user"
});

const blockMapWith = (child: any): BlockMapType =>
({
page: {
role: "reader",
value: {
...baseValue("page", "page", "root"),
properties: { title: [["Root"]] },
format: {},
permissions: [],
content: [child.value.id]
}
},
[child.value.id]: child
} as any);

describe("unsupported block rendering", () => {
beforeEach(() => {
jest.spyOn(console, "warn").mockImplementation(() => undefined);
});

afterEach(() => {
(console.warn as jest.Mock).mockRestore();
});

it("renders a helpful error for unsupported database blocks", () => {
const html = renderToStaticMarkup(
<NotionRenderer
blockMap={blockMapWith({
role: "reader",
value: baseValue("database", "collection_view")
})}
/>
);

expect(html).toContain("Unsupported Notion block");
expect(html).toContain(
"This Notion document includes a Database block which react-notion cannot render."
);
expect(html).toContain("Block ID: database");
});

it("can hide unsupported block errors for backwards compatibility", () => {
const html = renderToStaticMarkup(
<NotionRenderer
showUnsupportedBlockErrors={false}
blockMap={blockMapWith({
role: "reader",
value: baseValue("checkbox", "checkbox")
})}
/>
);

expect(html).not.toContain("Unsupported Notion block");
expect(html).not.toContain("Checkbox block");
});

it("allows unsupported block rendering to be customized", () => {
const html = renderToStaticMarkup(
<NotionRenderer
renderUnsupportedBlock={block => (
<span className="custom-error">{block.value.id}</span>
)}
blockMap={blockMapWith({
role: "reader",
value: baseValue("toc", "table_of_contents")
})}
/>
);

expect(html).toContain("custom-error");
expect(html).toContain("toc");
expect(html).not.toContain("Unsupported Notion block");
});

it("still renders collection views when collection data is present", () => {
const html = renderToStaticMarkup(
<NotionRenderer
blockMap={blockMapWith({
role: "reader",
value: baseValue("collection", "collection_view"),
collection: {
title: [["Tasks"]],
types: [
{
type: "table",
format: {
table_wrap: false,
table_properties: [
{ visible: true, property: "name", width: 120 }
]
}
}
],
data: [{ Name: [["Ship it"]] }],
schema: { name: { name: "Name", type: "title" } }
}
})}
/>
);

expect(html).toContain("Tasks");
expect(html).toContain("Ship it");
expect(html).not.toContain("Unsupported Notion block");
});
});