Skip to content
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

feat(sdk): React Block Editor Render Component #29653

Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9e86233
Working on BlockEditorRenderer component
kevindaviladev Aug 20, 2024
e24b0bf
WIP BlockEditorRenderer
kevindaviladev Aug 22, 2024
3e8f3f3
BlockEditorRenderer working. Added customRenderer, className and styl…
kevindaviladev Aug 22, 2024
2c789f3
Finished functionallity, no test/cleanup yet
kevindaviladev Aug 22, 2024
64b19d5
Finished functionallity. Added table and fixed mark render. Pending t…
kevindaviladev Aug 23, 2024
cb773c7
Added types and tests
kevindaviladev Aug 23, 2024
5a0aff3
Added JSDocs
kevindaviladev Aug 25, 2024
2b9836d
Little refactor on BlockEditorRenderer
kevindaviladev Aug 25, 2024
77da00c
Fix for lint
kevindaviladev Aug 25, 2024
d633a3a
Removed compilerOptions and change some interfaces
kevindaviladev Aug 26, 2024
8add7a2
Create global block mocks
kevindaviladev Aug 26, 2024
b6a1bba
Resolved SQ issues
kevindaviladev Aug 26, 2024
087c771
Changed key on for statements
kevindaviladev Aug 26, 2024
a641384
Changed key on for statements again. Now we use crypto.randomUUID (Na…
kevindaviladev Aug 26, 2024
1dea884
Added optinal chaining
kevindaviladev Aug 26, 2024
bc9792e
Back to use array index as key
kevindaviladev Aug 27, 2024
28194be
PR Suggestions I
kevindaviladev Aug 27, 2024
4384ad6
Exported interfaces to dev
kevindaviladev Aug 29, 2024
be88dd3
Manually merged with master
kevindaviladev Aug 29, 2024
fa62db5
Merged with master again. Downgraded lib versions to have only one ve…
kevindaviladev Aug 29, 2024
2b10697
exported all block interfaces
kevindaviladev Sep 3, 2024
e922064
Removed jsconfig config
kevindaviladev Sep 3, 2024
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
1 change: 1 addition & 0 deletions core-web/libs/sdk/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './lib/components/DotcmsLayout/DotcmsLayout';
export * from './lib/components/PageProvider/PageProvider';
export * from './lib/components/Row/Row';
export * from './lib/hooks/useDotcmsPageContext';
export * from './lib/components/BlockEditorRenderer/BlockEditorRenderer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';

import crypto from 'crypto';

import { BlockEditorRenderer } from './BlockEditorRenderer';

import { Block } from '../../models/blocks.interface';

Object.defineProperty(global.self, 'crypto', {
value: {
randomUUID: crypto.randomUUID
}
});

describe('BlockEditorRenderer', () => {
const blocks = {
content: [
{
type: 'paragraph',
attrs: {},
content: [
{
type: 'text',
text: 'Hello, World!'
}
]
}
]
} as Block;

it('should render the BlockEditorItem component', () => {
const { getByText } = render(<BlockEditorRenderer blocks={blocks} />);
expect(getByText('Hello, World!')).toBeInTheDocument();
});

it('should render the custom renderer component', () => {
const customRenderers = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
paragraph: ({ content }: { content: any }) => {
const [{ text }] = content;

return <p data-testid="custom-paragraph">{text}</p>;
}
};
const { getByTestId } = render(
<BlockEditorRenderer blocks={blocks} customRenderers={customRenderers} />
);
expect(getByTestId('custom-paragraph')).toBeInTheDocument();
});

it('should render the property className and style props', () => {
const { container } = render(
<BlockEditorRenderer blocks={blocks} className="test-class" style={{ color: 'red' }} />
);
expect(container.firstChild).toHaveClass('test-class');
expect(container.firstChild).toHaveStyle('color: red');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BlockEditorItem } from './item/BlockEditorItem';

import { Block, CustomRenderer } from '../../models/blocks.interface';

interface BlockEditorRendererProps {
blocks: Block;
customRenderers?: CustomRenderer;
className?: string;
style?: React.CSSProperties;
}
KevinDavilaDotCMS marked this conversation as resolved.
Show resolved Hide resolved

/**
* Renders a block editor with the specified blocks, custom renderers, className, and style.
*
* @param blocks - The blocks to be rendered in the editor.
* @param customRenderers - Custom renderers for specific block types.
* @param className - The CSS class name for the container element.
* @param style - The inline styles for the container element.
* @returns The rendered block editor.
*/
KevinDavilaDotCMS marked this conversation as resolved.
Show resolved Hide resolved
export const BlockEditorRenderer = ({
blocks,
customRenderers,
className,
style
}: BlockEditorRendererProps) => {
return (
<div className={className} style={style}>
<BlockEditorItem content={blocks.content} customRenderers={customRenderers} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BlockProps, ContentNode } from '../../../models/blocks.interface';

type CodeBlockProps = BlockProps & ContentNode;
KevinDavilaDotCMS marked this conversation as resolved.
Show resolved Hide resolved

/**
* Renders a code block component.
*
* @param attrs - The attributes of the code block.
* @param children - The content of the code block.
* @returns The rendered code block component.
*/
export const CodeBlock = ({ attrs, children }: CodeBlockProps) => {
const language = attrs?.language || '';

return (
<pre data-language={language}>
<code>{children}</code>
</pre>
);
};

/**
* Renders a blockquote component.
*
* @param children - The content to be rendered inside the blockquote.
* @returns The rendered blockquote component.
*/
export const BlockQuote = ({ children }: BlockProps) => {
return <blockquote>{children}</blockquote>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ContentNode, CustomRenderer } from '../../../models/blocks.interface';

// Replace this when we have a types lib
export interface Contentlet {
hostName: string;
modDate: string;
publishDate: string;
title: string;
baseType: string;
inode: string;
archived: boolean;
ownerName: string;
host: string;
working: boolean;
locked: boolean;
stInode: string;
contentType: string;
live: boolean;
owner: string;
identifier: string;
publishUserName: string;
publishUser: string;
languageId: number;
creationDate: string;
url: string;
titleImage: string;
modUserName: string;
hasLiveVersion: boolean;
folder: string;
hasTitleImage: boolean;
sortOrder: number;
modUser: string;
__icon__: string;
contentTypeIcon: string;
variant: string;
}

type DotContentProps = ContentNode & {
customRenderers?: CustomRenderer;
};
KevinDavilaDotCMS marked this conversation as resolved.
Show resolved Hide resolved

/**
* Renders the default content for an unknown content type.
*/
const DefaultContent = () => <div>Unknown Content Type</div>;

/**
* Renders a DotContent component.
*
* @param {DotContentProps} props - The props for the DotContent component.
* @returns {JSX.Element} The rendered DotContent component.
*/
export const DotContent = (props: DotContentProps) => {
const { attrs, customRenderers } = props;

const data = attrs?.data as unknown as Contentlet;

const Component = customRenderers?.[data?.contentType] ?? DefaultContent;

if (!data) {
console.error('DotContent: No data provided');
}

return <Component {...data} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DotCmsClient } from '@dotcms/client';

import { ContentNode } from '../../../models/blocks.interface';

type DotCMSImageProps = ContentNode['attrs'] & {
data?: Record<string, unknown>;
};

/**
* Renders an image component for dotCMS.
*
* @param props - The props for the DotCMSImage component.
* @returns The rendered image component.
*/
export const DotCMSImage = (props: ContentNode) => {
const { data, src, alt } = props.attrs as DotCMSImageProps;
const client = DotCmsClient.instance;

const srcUrl = data?.identifier ? `${client.dotcmsUrl}${src}` : src;

return <img alt={alt} src={srcUrl} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
interface BlockProps {
children: React.ReactNode;
}

/**
* ListItem component represents a list item in a block editor.
*
* @param children - The content of the list item.
* @returns The rendered list item element.
*/
export const ListItem = ({ children }: BlockProps) => {
return <li>{children}</li>;
};

/**
* Renders an ordered list component.
*
* @param children - The content to be rendered inside the ordered list.
* @returns The ordered list component.
*/
export const OrderedList = ({ children }: BlockProps) => {
return <ol>{children}</ol>;
};

/**
* Renders a bullet list component.
*
* @param children - The content of the bullet list.
* @returns The rendered bullet list component.
*/
export const BulletList = ({ children }: BlockProps) => {
return <ul>{children}</ul>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';

import { ContentNode } from '../../../models/blocks.interface';

interface TableRendererProps {
content: ContentNode[];
blockEditorItem: React.FC<{
content: ContentNode[];
}>;
}

/**
* Renders a table component for the Block Editor.
*
* @param content - The content of the table.
* @param blockEditorItem - The Block Editor item component.
*/
export const TableRenderer: React.FC<TableRendererProps> = ({
content,
blockEditorItem
}: TableRendererProps) => {
const BlockEditorItemComponent = blockEditorItem;

const renderTableContent = (node: ContentNode) => {
return <BlockEditorItemComponent content={node.content} />;
};

return (
<table>
<thead>
{content.slice(0, 1).map((rowNode) => (
<tr key={crypto.randomUUID()}>
{rowNode.content?.map((cellNode) => (
<th
key={crypto.randomUUID()}
colSpan={Number(cellNode.attrs?.colspan || 1)}
rowSpan={Number(cellNode.attrs?.rowspan || 1)}>
{renderTableContent(cellNode)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{content.slice(1).map((rowNode) => (
<tr key={crypto.randomUUID()}>
{rowNode.content?.map((cellNode) => (
<td
key={crypto.randomUUID()}
colSpan={Number(cellNode.attrs?.colspan || 1)}
rowSpan={Number(cellNode.attrs?.rowspan || 1)}>
{renderTableContent(cellNode)}
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
Loading