Skip to content

Commit 893649b

Browse files
authored
chore(BlockEditor): Block Editor Rendering Polishing (dotCMS#32242)
This pull request enhances error handling and developer experience for unknown block types and content types in the `DotCMSBlockEditorRenderer` component. Key changes include introducing specialized components for rendering warnings, improving error messages, and ensuring better feedback in development mode. ### Improvements to error handling and developer feedback: * **Handling unknown block types**: - Introduced a new `UnknownBlock` component to render a warning message for unknown block types in development mode. In production, it returns `null`. (`core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/BlockEditorBlock.tsx`, [core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/BlockEditorBlock.tsxL146-R164](diffhunk://#diff-39cc32a90ddc4a594fb12c16243c0e64f3c656e3e428a00bb0cbaa0b6479ae72L146-R164)) - Utilized the `getUVEState` function to determine the environment and conditionally render the warning. (`core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/BlockEditorBlock.tsx`, [core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/BlockEditorBlock.tsxR3](diffhunk://#diff-39cc32a90ddc4a594fb12c16243c0e64f3c656e3e428a00bb0cbaa0b6479ae72R3)) * **Handling unknown content types**: - Added a new `UnknownContentType` component to display a styled warning message for unrecognized content types, including a link to documentation for creating custom renderers. (`core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/UnknownContentType.tsx`, [core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/UnknownContentType.tsxR1-R25](diffhunk://#diff-ebfefde04dcec9691ba1bebfba0e9d5d3298fbe8b7268ee5482786ea71067b6eR1-R25)) - Updated the `DotContent` component to use `UnknownContentType` in development mode and log a more descriptive warning message in production. (`core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/Contentlet.tsx`, [core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/Contentlet.tsxL33-R46](diffhunk://#diff-887231e30163e16f3681710fc8ab01a44fe82613642b38a41c207367b55f6be5L33-R46)) ### Improvements to error messages: * Defined constants for error messages (`DOT_CONTENT_NO_DATA_MESSAGE` and `DOT_CONTENT_NO_MATCHING_COMPONENT_MESSAGE`) to ensure consistency and improve readability. (`core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/Contentlet.tsx`, [core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/Contentlet.tsxR13-R18](diffhunk://#diff-887231e30163e16f3681710fc8ab01a44fe82613642b38a41c207367b55f6be5R13-R18)) * Replaced generic error messages in the `DotContent` component with these constants for better clarity. (`core-web/libs/sdk/react/src/lib/next/components/DotCMSBlockEditorRenderer/components/blocks/Contentlet.tsx`, [[1]](diffhunk://#diff-887231e30163e16f3681710fc8ab01a44fe82613642b38a41c207367b55f6be5L23-R31) [[2]](diffhunk://#diff-887231e30163e16f3681710fc8ab01a44fe82613642b38a41c207367b55f6be5L33-R46) ### Video #### Angular https://github.com/user-attachments/assets/89ced808-735e-4929-b5ed-40d20aaf030c #### Next.js https://github.com/user-attachments/assets/9b25e7f3-bd12-4e43-97fd-c6036cce3eb8
1 parent 5b2ddf6 commit 893649b

File tree

17 files changed

+535
-133
lines changed

17 files changed

+535
-133
lines changed

core-web/libs/sdk/angular/next/components/dotcms-block-editor-renderer/blocks/contentlet.component.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator';
2+
3+
import { Component, Input } from '@angular/core';
4+
5+
import { BlockEditorNode, DotCMSBasicContentlet, UVE_MODE } from '@dotcms/types';
6+
import { getUVEState } from '@dotcms/uve';
7+
8+
import { DotContentletBlock, NoComponentProvided } from './dot-contentlet.component';
9+
10+
import { CustomRenderer } from '../dotcms-block-editor-renderer.component';
11+
12+
@Component({
13+
selector: 'test-component',
14+
template: '<div data-testId="test-component">Test Component</div>'
15+
})
16+
class TestComponent {
17+
@Input() contentlet!: DotCMSBasicContentlet;
18+
}
19+
20+
// Mock data
21+
const mockData: BlockEditorNode['attrs'] = {
22+
data: {
23+
contentType: 'test'
24+
}
25+
};
26+
27+
const mockCustomRenderers: CustomRenderer = {
28+
test: Promise.resolve(TestComponent)
29+
};
30+
31+
const MOCK_UVE_STATE_EDIT = {
32+
mode: UVE_MODE.EDIT,
33+
persona: 'test',
34+
variantName: 'test',
35+
experimentId: 'test',
36+
publishDate: 'test',
37+
languageId: 'test'
38+
};
39+
40+
jest.mock('@dotcms/uve', () => ({
41+
getUVEState: jest.fn()
42+
}));
43+
44+
// Test suite
45+
describe('DotContentletBlock', () => {
46+
const getUVEStateMock = getUVEState as jest.Mock;
47+
48+
let spectator: Spectator<DotContentletBlock>;
49+
const createComponent = createComponentFactory({
50+
component: DotContentletBlock,
51+
imports: [],
52+
mocks: [NoComponentProvided],
53+
detectChanges: false
54+
});
55+
56+
beforeEach(() => {
57+
spectator = createComponent({
58+
props: {
59+
attrs: mockData,
60+
customRenderers: mockCustomRenderers
61+
}
62+
});
63+
});
64+
65+
afterEach(() => {
66+
jest.clearAllMocks();
67+
});
68+
69+
it('should log a message if no data is provided', () => {
70+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {
71+
/* empty */
72+
});
73+
spectator.setInput('attrs', undefined);
74+
spectator.detectChanges();
75+
expect(consoleSpy).toHaveBeenCalledWith(
76+
'[DotCMSBlockEditorRenderer]: No data provided for Contentlet Block. Try to add a contentlet to the block editor. If the error persists, please contact the DotCMS support team.'
77+
);
78+
});
79+
80+
it('should use NoComponentProvided in dev mode if no component is found', () => {
81+
jest.spyOn(console, 'error').mockImplementation(() => {
82+
/* empty */
83+
});
84+
jest.spyOn(console, 'warn').mockImplementation(() => {
85+
/* empty */
86+
});
87+
getUVEStateMock.mockReturnValue(MOCK_UVE_STATE_EDIT);
88+
spectator.setInput('customRenderers', {});
89+
spectator.detectChanges();
90+
const unknownContentType = spectator.query(byTestId('no-component-provided'));
91+
expect(unknownContentType).toBeTruthy();
92+
});
93+
94+
it('should log a warning and render nothing if no component is found', () => {
95+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {
96+
/* empty */
97+
});
98+
spectator.setInput('customRenderers', {});
99+
spectator.detectChanges();
100+
expect(consoleSpy).toHaveBeenCalledWith(
101+
'[DotCMSBlockEditorRenderer]: No matching component found for content type: test. Provide a custom renderer for this content type to fix this error.'
102+
);
103+
expect(spectator.query('ng-container')).toBeNull();
104+
});
105+
106+
it('should render the component if it exists', async () => {
107+
spectator.detectChanges();
108+
109+
await spectator.fixture.whenStable();
110+
111+
const unknownContentType = spectator.query(byTestId('no-component-provided'));
112+
expect(unknownContentType).toBeFalsy();
113+
114+
spectator.detectChanges();
115+
116+
const testComponent = spectator.query(byTestId('test-component'));
117+
expect(testComponent).toBeTruthy();
118+
});
119+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { AsyncPipe, NgComponentOutlet } from '@angular/common';
2+
import { ChangeDetectionStrategy, Component, computed, Input } from '@angular/core';
3+
4+
import { BlockEditorNode, UVE_MODE } from '@dotcms/types';
5+
import { getUVEState } from '@dotcms/uve';
6+
7+
import { DynamicComponentEntity } from '../../../models';
8+
import { CustomRenderer } from '../dotcms-block-editor-renderer.component';
9+
10+
@Component({
11+
selector: 'dotcms-no-component-provided',
12+
standalone: true,
13+
template: `
14+
<div data-testid="no-component-provided" [style]="style">
15+
<strong style="color: #c05621">Dev Warning</strong>
16+
: No component or custom renderer provided for content type
17+
<strong style="color: #c05621">{{ contentType || 'Unknown' }}</strong>
18+
.
19+
<br />
20+
Please refer to the
21+
<a
22+
href="https://dev.dotcms.com/docs/block-editor"
23+
target="_blank"
24+
rel="noopener noreferrer"
25+
style="color: #c05621">
26+
Block Editor Custom Renderers Documentation
27+
</a>
28+
for guidance.
29+
</div>
30+
`
31+
})
32+
export class NoComponentProvided {
33+
@Input() contentType: string | undefined;
34+
protected readonly style = {
35+
backgroundColor: '#fffaf0',
36+
color: '#333',
37+
padding: '1rem',
38+
borderRadius: '0.5rem',
39+
marginBottom: '1rem',
40+
marginTop: '1rem',
41+
border: '1px solid #ed8936'
42+
};
43+
}
44+
45+
/**
46+
* DotContent component that renders content based on content type
47+
*/
48+
@Component({
49+
selector: 'dotcms-block-editor-renderer-contentlet',
50+
standalone: true,
51+
imports: [NgComponentOutlet, AsyncPipe, NoComponentProvided],
52+
changeDetection: ChangeDetectionStrategy.OnPush,
53+
template: `
54+
@if (contentComponent) {
55+
<ng-container
56+
*ngComponentOutlet="
57+
contentComponent | async;
58+
inputs: { contentlet: $data() }
59+
"></ng-container>
60+
} @else if (isDevMode) {
61+
<dotcms-no-component-provided [contentType]="$data()?.contentType" />
62+
}
63+
`
64+
})
65+
export class DotContentletBlock {
66+
@Input() customRenderers: CustomRenderer | undefined;
67+
@Input() attrs: BlockEditorNode['attrs'];
68+
69+
contentComponent: DynamicComponentEntity | undefined;
70+
protected readonly $data = computed(() => this.attrs?.['data']);
71+
private readonly DOT_CONTENT_NO_DATA_MESSAGE =
72+
'[DotCMSBlockEditorRenderer]: No data provided for Contentlet Block. Try to add a contentlet to the block editor. If the error persists, please contact the DotCMS support team.';
73+
private readonly DOT_CONTENT_NO_MATCHING_COMPONENT_MESSAGE = (contentType: string) =>
74+
`[DotCMSBlockEditorRenderer]: No matching component found for content type: ${contentType}. Provide a custom renderer for this content type to fix this error.`;
75+
protected get isDevMode() {
76+
return getUVEState()?.mode === UVE_MODE.EDIT;
77+
}
78+
79+
ngOnInit() {
80+
if (!this.$data()) {
81+
console.error(this.DOT_CONTENT_NO_DATA_MESSAGE);
82+
83+
return;
84+
}
85+
86+
const contentType = this.$data()?.contentType || '';
87+
this.contentComponent = this.customRenderers?.[contentType];
88+
89+
if (!this.contentComponent) {
90+
console.warn(this.DOT_CONTENT_NO_MATCHING_COMPONENT_MESSAGE(contentType));
91+
}
92+
}
93+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Component, Input } from '@angular/core';
2+
3+
import { BlockEditorNode, UVE_MODE } from '@dotcms/types';
4+
import { getUVEState } from '@dotcms/uve';
5+
6+
@Component({
7+
selector: 'dotcms-block-editor-renderer-unknown',
8+
standalone: true,
9+
template: `
10+
@if (isEditMode) {
11+
<div [style]="style" data-testid="unknown-block-type">
12+
<strong style="color: #c53030">Warning:</strong>
13+
The block type
14+
<strong>{{ node.type }}</strong>
15+
is not recognized. Please check your
16+
<a
17+
href="https://dev.dotcms.com/docs/block-editor"
18+
target="_blank"
19+
rel="noopener noreferrer">
20+
configuration
21+
</a>
22+
or contact support for assistance.
23+
</div>
24+
}
25+
`
26+
})
27+
export class DotUnknownBlockComponent {
28+
@Input() node!: BlockEditorNode;
29+
30+
get isEditMode() {
31+
return getUVEState()?.mode === UVE_MODE.EDIT;
32+
}
33+
34+
protected readonly style = {
35+
backgroundColor: '#fff5f5',
36+
color: '#333',
37+
padding: '1rem',
38+
borderRadius: '0.5rem',
39+
marginBottom: '1rem',
40+
marginTop: '1rem',
41+
border: '1px solid #fc8181'
42+
};
43+
}

core-web/libs/sdk/angular/next/components/dotcms-block-editor-renderer/dotcms-block-editor-renderer.component.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
{{ $blockEditorState().error }}
44
</div>
55
} @else if (!$blockEditorState().error) {
6-
<dotcms-block-editor-renderer-block
7-
[content]="blocks.content"
8-
[customRenderers]="customRenderers" />
6+
<div [class]="class" [style]="style">
7+
<dotcms-block-editor-renderer-block
8+
[content]="blocks.content"
9+
[customRenderers]="customRenderers" />
10+
</div>
911
}

core-web/libs/sdk/angular/next/components/dotcms-block-editor-renderer/dotcms-block-editor-renderer.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export type CustomRenderer = Record<string, DynamicComponentEntity>;
4343
export class DotCMSBlockEditorRendererComponent {
4444
@Input() blocks!: BlockEditorContent;
4545
@Input() customRenderers: CustomRenderer | undefined;
46+
@Input() class: string | undefined;
47+
@Input() style: string | Record<string, string> | undefined;
4648

4749
$blockEditorState = signal<BlockEditorState>({ error: null });
4850
$isInEditMode = signal(getUVEState()?.mode === UVE_MODE.EDIT);

core-web/libs/sdk/angular/next/components/dotcms-block-editor-renderer/item/dotcms-block-editor-item.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
}
9797

9898
@default {
99-
<div>Unknown Block Type: {{ node.type }}</div>
99+
<dotcms-block-editor-renderer-unknown [node]="node" />
100100
}
101101
}
102102
}

core-web/libs/sdk/angular/next/components/dotcms-block-editor-renderer/item/dotcms-block-editor-item.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { BlockEditorNode } from '@dotcms/types';
55
import { BlockEditorDefaultBlocks } from '@dotcms/types/internal';
66

77
import { DotCodeBlock, DotBlockQuote } from '../blocks/code.component';
8-
import { DotContentletBlock } from '../blocks/contentlet.component';
8+
import { DotContentletBlock } from '../blocks/dot-contentlet.component';
99
import { DotImageBlock } from '../blocks/image.component';
1010
import { DotBulletList, DotOrdererList, DotListItem } from '../blocks/list.component';
1111
import { DotTableBlock } from '../blocks/table.component';
1212
import { DotParagraphBlock, DotTextBlock, DotHeadingBlock } from '../blocks/text.component';
13+
import { DotUnknownBlockComponent } from '../blocks/unknown.component';
1314
import { DotVideoBlock } from '../blocks/video.component';
1415
import { CustomRenderer } from '../dotcms-block-editor-renderer.component';
1516

@@ -33,7 +34,8 @@ import { CustomRenderer } from '../dotcms-block-editor-renderer.component';
3334
DotImageBlock,
3435
DotVideoBlock,
3536
DotTableBlock,
36-
DotContentletBlock
37+
DotContentletBlock,
38+
DotUnknownBlockComponent
3739
]
3840
})
3941
export class DotCMSBlockEditorItemComponent {

0 commit comments

Comments
 (0)