@dotcms/react 0.0.1-alpha.4 → 0.0.1-alpha.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.babelrc +12 -0
  2. package/.eslintrc.json +18 -0
  3. package/README.md +0 -1
  4. package/jest.config.ts +11 -0
  5. package/package.json +6 -8
  6. package/project.json +56 -0
  7. package/src/{index.d.ts → index.ts} +3 -1
  8. package/src/lib/components/BlockEditorRenderer/BlockEditorRenderer.spec.tsx +51 -0
  9. package/src/lib/components/BlockEditorRenderer/BlockEditorRenderer.tsx +35 -0
  10. package/src/lib/components/BlockEditorRenderer/blocks/Code.tsx +29 -0
  11. package/src/lib/components/BlockEditorRenderer/blocks/Contentlet.tsx +61 -0
  12. package/src/lib/components/BlockEditorRenderer/blocks/Image.tsx +18 -0
  13. package/src/lib/components/BlockEditorRenderer/blocks/Lists.tsx +31 -0
  14. package/src/lib/components/BlockEditorRenderer/blocks/Table.tsx +60 -0
  15. package/src/lib/components/BlockEditorRenderer/blocks/Texts.tsx +126 -0
  16. package/src/lib/components/BlockEditorRenderer/blocks/Video.tsx +26 -0
  17. package/src/lib/components/BlockEditorRenderer/item/BlockEditorBlock.spec.tsx +634 -0
  18. package/src/lib/components/BlockEditorRenderer/item/BlockEditorBlock.tsx +146 -0
  19. package/src/lib/components/Column/Column.module.css +99 -0
  20. package/src/lib/components/Column/Column.spec.tsx +78 -0
  21. package/src/lib/components/Column/Column.tsx +59 -0
  22. package/src/lib/components/Container/Container.module.css +7 -0
  23. package/src/lib/components/Container/Container.spec.tsx +155 -0
  24. package/src/lib/components/Container/Container.tsx +122 -0
  25. package/src/lib/components/DotEditableText/DotEditableText.spec.tsx +232 -0
  26. package/src/lib/components/DotEditableText/DotEditableText.tsx +168 -0
  27. package/src/lib/components/DotEditableText/utils.ts +82 -0
  28. package/src/lib/components/DotcmsLayout/DotcmsLayout.module.css +7 -0
  29. package/src/lib/components/DotcmsLayout/DotcmsLayout.spec.tsx +46 -0
  30. package/src/lib/components/DotcmsLayout/DotcmsLayout.tsx +50 -0
  31. package/src/lib/components/PageProvider/PageProvider.module.css +7 -0
  32. package/src/lib/components/PageProvider/PageProvider.spec.tsx +59 -0
  33. package/src/lib/components/PageProvider/PageProvider.tsx +23 -0
  34. package/src/lib/components/Row/Row.module.css +5 -0
  35. package/src/lib/components/Row/Row.spec.tsx +92 -0
  36. package/src/lib/components/Row/Row.tsx +52 -0
  37. package/src/lib/contexts/PageContext.tsx +10 -0
  38. package/src/lib/hooks/useDotcmsEditor.spec.ts +176 -0
  39. package/src/lib/hooks/useDotcmsEditor.ts +94 -0
  40. package/src/lib/hooks/useDotcmsPageContext.spec.tsx +47 -0
  41. package/src/lib/hooks/useDotcmsPageContext.tsx +15 -0
  42. package/src/lib/mocks/mockPageContext.tsx +113 -0
  43. package/src/lib/models/blocks.interface.ts +81 -0
  44. package/src/lib/models/content-node.interface.ts +90 -0
  45. package/src/lib/models/index.ts +130 -0
  46. package/src/lib/utils/utils.ts +89 -0
  47. package/tsconfig.json +20 -0
  48. package/tsconfig.lib.json +23 -0
  49. package/tsconfig.spec.json +20 -0
  50. package/index.esm.d.ts +0 -1
  51. package/index.esm.js +0 -2911
  52. package/src/lib/components/Column/Column.d.ts +0 -5
  53. package/src/lib/components/Container/Container.d.ts +0 -5
  54. package/src/lib/components/DotcmsLayout/DotcmsLayout.d.ts +0 -32
  55. package/src/lib/components/PageProvider/PageProvider.d.ts +0 -82
  56. package/src/lib/components/Row/Row.d.ts +0 -25
  57. package/src/lib/contexts/PageContext.d.ts +0 -3
  58. package/src/lib/hooks/useDotcmsPageContext.d.ts +0 -9
  59. package/src/lib/hooks/usePageEditor.d.ts +0 -50
  60. package/src/lib/mocks/mockPageContext.d.ts +0 -7
  61. package/src/lib/utils/utils.d.ts +0 -55
  62. /package/src/lib/mocks/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,232 @@
1
+ import { expect } from '@jest/globals';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import * as tinymceReact from '@tinymce/tinymce-react';
4
+
5
+ import * as dotcmsClient from '@dotcms/client';
6
+
7
+ import { DotEditableText } from './DotEditableText';
8
+
9
+ import { dotcmsContentletMock } from '../../mocks/mockPageContext';
10
+
11
+ const { CUSTOMER_ACTIONS, postMessageToEditor } = dotcmsClient;
12
+
13
+ // Define mockEditor before using it in jest.mock
14
+ const TINYMCE_EDITOR_MOCK = {
15
+ focus: () => {
16
+ /* */
17
+ },
18
+ getContent: (_data: string) => '',
19
+ isDirty: () => false,
20
+ hasFocus: () => false,
21
+ setContent: () => {
22
+ /* */
23
+ }
24
+ };
25
+
26
+ jest.mock('@tinymce/tinymce-react', () => ({
27
+ Editor: jest.fn(({ onInit, onMouseDown, onFocusOut }) => {
28
+ onInit({}, TINYMCE_EDITOR_MOCK);
29
+
30
+ return <div data-testid="tinymce-editor" onMouseDown={onMouseDown} onBlur={onFocusOut} />;
31
+ })
32
+ }));
33
+
34
+ // Mock @dotcms/client module
35
+ jest.mock('@dotcms/client', () => ({
36
+ ...jest.requireActual('@dotcms/client'),
37
+ isInsideEditor: jest.fn().mockImplementation(() => true),
38
+ postMessageToEditor: jest.fn(),
39
+ DotCmsClient: {
40
+ dotcmsUrl: 'http://localhost:8080'
41
+ }
42
+ }));
43
+
44
+ const mockedDotcmsClient = dotcmsClient as jest.Mocked<typeof dotcmsClient>;
45
+ const { Editor } = tinymceReact as jest.Mocked<typeof tinymceReact>;
46
+
47
+ describe('DotEditableText', () => {
48
+ describe('Outside editor', () => {
49
+ beforeEach(() => {
50
+ mockedDotcmsClient.isInsideEditor.mockReturnValue(false);
51
+ render(<DotEditableText contentlet={dotcmsContentletMock} fieldName="title" />);
52
+ });
53
+
54
+ it('should render the content', () => {
55
+ const editor = screen.queryByTestId('tinymce-editor');
56
+ expect(editor).toBeNull();
57
+ expect(screen.getByText(dotcmsContentletMock['title'])).not.toBeNull();
58
+ });
59
+ });
60
+
61
+ describe('Inside editor', () => {
62
+ let rerenderFn: (ui: React.ReactNode) => void;
63
+
64
+ beforeEach(() => {
65
+ mockedDotcmsClient.isInsideEditor.mockReturnValue(true);
66
+ const { rerender } = render(
67
+ <DotEditableText contentlet={dotcmsContentletMock} fieldName="title" />
68
+ );
69
+ rerenderFn = rerender;
70
+ });
71
+
72
+ it('should pass the correct props to the Editor component', () => {
73
+ const editor = screen.getByTestId('tinymce-editor');
74
+ expect(editor).not.toBeNull();
75
+
76
+ expect(Editor).toHaveBeenCalledWith(
77
+ {
78
+ tinymceScriptSrc: 'http://localhost:8080/ext/tinymcev7/tinymce.min.js',
79
+ inline: true,
80
+ init: {
81
+ inline: true,
82
+ menubar: false,
83
+ plugins: '',
84
+ powerpaste_html_import: 'clean',
85
+ powerpaste_word_import: 'clean',
86
+ suffix: '.min',
87
+ toolbar: '',
88
+ valid_styles: {
89
+ '*': 'font-size,font-family,color,text-decoration,text-align'
90
+ }
91
+ },
92
+ initialValue: dotcmsContentletMock.title,
93
+ onMouseDown: expect.any(Function),
94
+ onFocusOut: expect.any(Function),
95
+ onInit: expect.any(Function)
96
+ },
97
+ {}
98
+ );
99
+ });
100
+
101
+ describe('DotEditableText events', () => {
102
+ let focusSpy: jest.SpyInstance;
103
+
104
+ describe('Window Message', () => {
105
+ beforeEach(() => {
106
+ focusSpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'focus');
107
+ });
108
+
109
+ it("should focus on the editor when the message is 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS'", () => {
110
+ window.dispatchEvent(
111
+ new MessageEvent('message', {
112
+ data: {
113
+ name: 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS',
114
+ payload: {
115
+ oldInode: dotcmsContentletMock['inode'],
116
+ inode: '456'
117
+ }
118
+ }
119
+ })
120
+ );
121
+ expect(focusSpy).toHaveBeenCalled();
122
+ });
123
+
124
+ it("should not focus on the editor when the message is not 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS'", () => {
125
+ window.dispatchEvent(
126
+ new MessageEvent('message', {
127
+ data: { name: 'ANOTHER_EVENT' }
128
+ })
129
+ );
130
+ expect(focusSpy).not.toHaveBeenCalled();
131
+ });
132
+ });
133
+
134
+ describe('mousedown', () => {
135
+ const event = new MouseEvent('mousedown', {
136
+ bubbles: true
137
+ });
138
+ const mutiplePagesContentlet = {
139
+ ...dotcmsContentletMock,
140
+ onNumberOfPages: 2
141
+ };
142
+
143
+ it('should postMessage the UVE if the content is in multiple pages', () => {
144
+ rerenderFn(
145
+ <DotEditableText contentlet={mutiplePagesContentlet} fieldName="title" />
146
+ );
147
+ const editorElem = screen.getByTestId('tinymce-editor');
148
+ fireEvent(editorElem, event);
149
+
150
+ const payload = {
151
+ dataset: {
152
+ fieldName: 'title',
153
+ inode: mutiplePagesContentlet.inode,
154
+ language: mutiplePagesContentlet.languageId
155
+ }
156
+ };
157
+ expect(postMessageToEditor).toHaveBeenCalledWith({
158
+ action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING,
159
+ payload
160
+ });
161
+ });
162
+
163
+ it('should not postMessage the UVE if the content is in a single page', () => {
164
+ const editorElem = screen.getByTestId('tinymce-editor');
165
+ fireEvent(editorElem, event);
166
+ expect(postMessageToEditor).not.toHaveBeenCalled();
167
+ });
168
+ });
169
+
170
+ describe('onFocusOut', () => {
171
+ let isDirtySpy: jest.SpyInstance;
172
+ let getContentSpy: jest.SpyInstance;
173
+
174
+ const event = new FocusEvent('focusout', {
175
+ bubbles: true
176
+ });
177
+
178
+ beforeEach(() => {
179
+ isDirtySpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'isDirty');
180
+ getContentSpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'getContent');
181
+ });
182
+
183
+ it('should not postMessage the UVE if the editor is not dirty', () => {
184
+ mockedDotcmsClient.isInsideEditor.mockReturnValue(false);
185
+ const editorElem = screen.getByTestId('tinymce-editor');
186
+ fireEvent(editorElem, event);
187
+ expect(isDirtySpy).toHaveBeenCalled();
188
+ expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' });
189
+ expect(postMessageToEditor).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it('should not postMessage the UVE if the content did not change', () => {
193
+ isDirtySpy.mockReturnValue(true);
194
+ getContentSpy.mockReturnValue(dotcmsContentletMock.title);
195
+
196
+ const editorElem = screen.getByTestId('tinymce-editor');
197
+ fireEvent(editorElem, event);
198
+
199
+ expect(isDirtySpy).toHaveBeenCalled();
200
+ expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' });
201
+ expect(postMessageToEditor).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it('should postMessage the UVE if the content changed', () => {
205
+ isDirtySpy.mockReturnValue(true);
206
+ getContentSpy.mockReturnValue('New content');
207
+
208
+ const editorElem = screen.getByTestId('tinymce-editor');
209
+ fireEvent(editorElem, event);
210
+
211
+ const postMessageData = {
212
+ action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING,
213
+ payload: {
214
+ content: 'New content',
215
+ dataset: {
216
+ inode: dotcmsContentletMock.inode,
217
+ langId: dotcmsContentletMock.languageId,
218
+ fieldName: 'title'
219
+ }
220
+ }
221
+ };
222
+
223
+ expect(isDirtySpy).toHaveBeenCalled();
224
+ expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' });
225
+ expect(postMessageToEditor).toHaveBeenCalledWith(postMessageData);
226
+ });
227
+ });
228
+ });
229
+ });
230
+
231
+ afterEach(() => jest.clearAllMocks()); // Clear all mocks to avoid side effects from other tests
232
+ });
@@ -0,0 +1,168 @@
1
+ import { Editor } from '@tinymce/tinymce-react';
2
+ import { useEffect, useRef, useState } from 'react';
3
+
4
+ import {
5
+ isInsideEditor as isInsideEditorFn,
6
+ postMessageToEditor,
7
+ CUSTOMER_ACTIONS,
8
+ DotCmsClient
9
+ } from '@dotcms/client';
10
+
11
+ import { DotEditableTextProps, TINYMCE_CONFIG } from './utils';
12
+
13
+ const MCE_URL = '/ext/tinymcev7/tinymce.min.js';
14
+
15
+ /**
16
+ * Allows inline edit content pulled from dotCMS API using TinyMCE editor
17
+ *
18
+ * @export
19
+ * @component
20
+ * @param {Readonly<DotEditableTextProps>} props {
21
+ * mode = 'plain',
22
+ * format = 'text',
23
+ * contentlet,
24
+ * fieldName = ''
25
+ * }
26
+ * @example
27
+ * ```javascript
28
+ * import { DotEditableText } from '@dotcms/react';
29
+ *
30
+ * const MyContentletWithTitle = ({ contentlet }) => (
31
+ * <h2>
32
+ * <DotEditableText
33
+ * contentlet={contentlet}
34
+ * fieldName="title"
35
+ * mode='full'
36
+ * format='text'/>
37
+ * </h2>
38
+ * );
39
+ * ```
40
+ * @returns {JSX.Element} A component to edit content inline
41
+ */
42
+ export function DotEditableText({
43
+ mode = 'plain',
44
+ format = 'text',
45
+ contentlet,
46
+ fieldName = ''
47
+ }: Readonly<DotEditableTextProps>): JSX.Element {
48
+ const editorRef = useRef<Editor['editor'] | null>(null);
49
+ const [scriptSrc, setScriptSrc] = useState('');
50
+ const [isInsideEditor, setIsInsideEditor] = useState(false);
51
+ const [content, setContent] = useState(contentlet?.[fieldName] || '');
52
+
53
+ useEffect(() => {
54
+ setIsInsideEditor(isInsideEditorFn());
55
+
56
+ if (!contentlet || !fieldName) {
57
+ console.error('DotEditableText: contentlet or fieldName is missing');
58
+ console.error('Ensure that all needed props are passed to view and edit the content');
59
+
60
+ return;
61
+ }
62
+
63
+ if (!isInsideEditorFn()) {
64
+ return;
65
+ }
66
+
67
+ const createURL = new URL(MCE_URL, DotCmsClient.dotcmsUrl);
68
+ setScriptSrc(createURL.toString());
69
+
70
+ const content = contentlet?.[fieldName] || '';
71
+ editorRef.current?.setContent(content, { format });
72
+ setContent(content);
73
+ }, [format, fieldName, contentlet]);
74
+
75
+ useEffect(() => {
76
+ if (!isInsideEditorFn()) {
77
+ return;
78
+ }
79
+
80
+ const onMessage = ({ data }: MessageEvent) => {
81
+ const { name, payload } = data;
82
+ if (name !== 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS') {
83
+ return;
84
+ }
85
+
86
+ const { oldInode, inode } = payload;
87
+ const currentInode = contentlet.inode;
88
+ const shouldFocus = currentInode === oldInode || currentInode === inode;
89
+
90
+ if (shouldFocus) {
91
+ editorRef.current?.focus();
92
+ }
93
+ };
94
+
95
+ window.addEventListener('message', onMessage);
96
+
97
+ return () => {
98
+ window.removeEventListener('message', onMessage);
99
+ };
100
+ }, [contentlet.inode]);
101
+
102
+ const onMouseDown = (event: MouseEvent) => {
103
+ const { onNumberOfPages = 1 } = contentlet;
104
+ const { inode, languageId: language } = contentlet;
105
+
106
+ if (onNumberOfPages <= 1 || editorRef.current?.hasFocus()) {
107
+ return;
108
+ }
109
+
110
+ event.stopPropagation();
111
+ event.preventDefault();
112
+
113
+ postMessageToEditor({
114
+ action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING,
115
+ payload: {
116
+ dataset: {
117
+ inode,
118
+ language,
119
+ fieldName
120
+ }
121
+ }
122
+ });
123
+ };
124
+
125
+ const onFocusOut = () => {
126
+ const editedContent = editorRef.current?.getContent({ format: format }) || '';
127
+ const { inode, languageId: langId } = contentlet;
128
+
129
+ if (!editorRef.current?.isDirty() || !didContentChange(editedContent)) {
130
+ return;
131
+ }
132
+
133
+ postMessageToEditor({
134
+ action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING,
135
+ payload: {
136
+ content: editedContent,
137
+ dataset: {
138
+ inode,
139
+ langId,
140
+ fieldName
141
+ }
142
+ }
143
+ });
144
+ };
145
+
146
+ const didContentChange = (editedContent: string) => {
147
+ return content !== editedContent;
148
+ };
149
+
150
+ if (!isInsideEditor) {
151
+ // We can let the user pass the Child Component and create a root to get the HTML for the editor
152
+ return <span dangerouslySetInnerHTML={{ __html: content }} />;
153
+ }
154
+
155
+ return (
156
+ <Editor
157
+ tinymceScriptSrc={scriptSrc}
158
+ inline={true}
159
+ onInit={(_, editor) => (editorRef.current = editor)}
160
+ init={TINYMCE_CONFIG[mode]}
161
+ initialValue={content}
162
+ onMouseDown={onMouseDown}
163
+ onFocusOut={onFocusOut}
164
+ />
165
+ );
166
+ }
167
+
168
+ export default DotEditableText;
@@ -0,0 +1,82 @@
1
+ import { IAllProps } from '@tinymce/tinymce-react';
2
+
3
+ import { DotCMSContentlet } from '../../models';
4
+
5
+ export type DOT_EDITABLE_TEXT_FORMAT = 'html' | 'text';
6
+
7
+ export type DOT_EDITABLE_TEXT_MODE = 'minimal' | 'full' | 'plain';
8
+
9
+ export interface DotEditableTextProps {
10
+ /**
11
+ * Represents the field name of the `contentlet` that can be edited
12
+ *
13
+ * @memberof DotEditableTextProps
14
+ */
15
+ fieldName: string;
16
+ /**
17
+ * Represents the format of the editor which can be `text` or `html`
18
+ *
19
+ * @type {DOT_EDITABLE_TEXT_FORMAT}
20
+ * @memberof DotEditableTextProps
21
+ */
22
+ format?: DOT_EDITABLE_TEXT_FORMAT;
23
+ /**
24
+ * Represents the mode of the editor which can be `plain`, `minimal`, or `full`
25
+ *
26
+ * @type {DOT_EDITABLE_TEXT_MODE}
27
+ * @memberof DotEditableTextProps
28
+ */
29
+ mode?: DOT_EDITABLE_TEXT_MODE;
30
+ /**
31
+ * Represents the `contentlet` that can be inline edited
32
+ *
33
+ * @type {DotCMSContentlet}
34
+ * @memberof DotEditableTextProps
35
+ */
36
+ contentlet: DotCMSContentlet;
37
+ }
38
+
39
+ const DEFAULT_TINYMCE_CONFIG: IAllProps['init'] = {
40
+ inline: true,
41
+ menubar: false,
42
+ powerpaste_html_import: 'clean',
43
+ powerpaste_word_import: 'clean',
44
+ suffix: '.min',
45
+ valid_styles: {
46
+ '*': 'font-size,font-family,color,text-decoration,text-align'
47
+ }
48
+ };
49
+
50
+ export const TINYMCE_CONFIG: {
51
+ [key in DOT_EDITABLE_TEXT_MODE]: IAllProps['init'];
52
+ } = {
53
+ full: {
54
+ ...DEFAULT_TINYMCE_CONFIG,
55
+ plugins: 'link lists autolink charmap',
56
+ toolbar: [
57
+ 'styleselect undo redo | bold italic underline | forecolor backcolor | alignleft aligncenter alignright alignfull | numlist bullist outdent indent | hr charmap removeformat | link'
58
+ ],
59
+ style_formats: [
60
+ { title: 'Paragraph', format: 'p' },
61
+ { title: 'Header 1', format: 'h1' },
62
+ { title: 'Header 2', format: 'h2' },
63
+ { title: 'Header 3', format: 'h3' },
64
+ { title: 'Header 4', format: 'h4' },
65
+ { title: 'Header 5', format: 'h5' },
66
+ { title: 'Header 6', format: 'h6' },
67
+ { title: 'Pre', format: 'pre' },
68
+ { title: 'Code', format: 'code' }
69
+ ]
70
+ },
71
+ plain: {
72
+ ...DEFAULT_TINYMCE_CONFIG,
73
+ plugins: '',
74
+ toolbar: ''
75
+ },
76
+ minimal: {
77
+ ...DEFAULT_TINYMCE_CONFIG,
78
+ plugins: 'link autolink',
79
+ toolbar: 'bold italic underline | link',
80
+ valid_elements: 'strong,em,span[style],a[href]'
81
+ }
82
+ };
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Replace this with your own classes
3
+ *
4
+ * e.g.
5
+ * .container {
6
+ * }
7
+ */
@@ -0,0 +1,46 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { ElementRef, ForwardRefExoticComponent } from 'react';
4
+
5
+ import { DotcmsLayout } from './DotcmsLayout';
6
+
7
+ import { mockPageContext } from '../../mocks/mockPageContext';
8
+
9
+ jest.mock('../Row/Row', () => {
10
+ const { forwardRef } = jest.requireActual('react');
11
+
12
+ return {
13
+ Row: forwardRef(
14
+ (
15
+ { children }: { children: JSX.Element },
16
+ ref: ElementRef<ForwardRefExoticComponent<unknown>>
17
+ ) => (
18
+ <div data-testid="mockRow" ref={ref}>
19
+ {children}
20
+ </div>
21
+ )
22
+ )
23
+ };
24
+ });
25
+
26
+ jest.mock('../PageProvider/PageProvider', () => {
27
+ return {
28
+ PageProvider: ({ children }: { children: JSX.Element }) => (
29
+ <div data-testid="mockPageProvider">{children}</div>
30
+ )
31
+ };
32
+ });
33
+
34
+ describe('DotcmsLayout', () => {
35
+ it('renders correctly with PageProvider and rows', () => {
36
+ render(
37
+ <DotcmsLayout
38
+ pageContext={{ ...mockPageContext, isInsideEditor: true }}
39
+ config={{ pathname: 'some-url' }}
40
+ />
41
+ );
42
+ expect(screen.getAllByTestId('mockRow').length).toBe(
43
+ mockPageContext.pageAsset.layout.body.rows.length
44
+ );
45
+ });
46
+ });
@@ -0,0 +1,50 @@
1
+ import { DotCMSPageEditorConfig } from '@dotcms/client';
2
+
3
+ import { useDotcmsEditor } from '../../hooks/useDotcmsEditor';
4
+ import { DotCMSPageContext } from '../../models';
5
+ import { PageProvider } from '../PageProvider/PageProvider';
6
+ import { Row } from '../Row/Row';
7
+
8
+ /**
9
+ * `DotcmsPageProps` is a type that defines the properties for the `DotcmsLayout` component.
10
+ * It includes a readonly `entity` property that represents the context for a DotCMS page.
11
+ *
12
+ * @typedef {Object} DotcmsPageProps
13
+ *
14
+ * @property {DotCMSPageContext} entity - The context for a DotCMS page.
15
+ * @readonly
16
+ */
17
+ export type DotcmsPageProps = {
18
+ /**
19
+ * `pageContext` is a readonly property of the `DotcmsPageProps` type.
20
+ * It represents the context for a DotCMS page and is of type `PageProviderContext`.
21
+ *
22
+ * @property {PageProviderContext} pageContext
23
+ * @memberof DotcmsPageProps
24
+ * @type {DotCMSPageContext}
25
+ * @readonly
26
+ */
27
+ readonly pageContext: DotCMSPageContext;
28
+
29
+ readonly config: DotCMSPageEditorConfig;
30
+ };
31
+
32
+ /**
33
+ * `DotcmsLayout` is a functional component that renders a layout for a DotCMS page.
34
+ * It takes a `DotcmsPageProps` object as a parameter and returns a JSX element.
35
+ *
36
+ * @category Components
37
+ * @param {DotcmsPageProps} props - The properties for the DotCMS page.
38
+ * @returns {JSX.Element} - A JSX element that represents the layout for a DotCMS page.
39
+ */
40
+ export function DotcmsLayout(dotPageProps: DotcmsPageProps): JSX.Element {
41
+ const pageContext = useDotcmsEditor(dotPageProps);
42
+
43
+ return (
44
+ <PageProvider pageContext={pageContext}>
45
+ {pageContext.pageAsset?.layout?.body.rows.map((row, index) => (
46
+ <Row key={index} row={row} />
47
+ ))}
48
+ </PageProvider>
49
+ );
50
+ }
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Replace this with your own classes
3
+ *
4
+ * e.g.
5
+ * .container {
6
+ * }
7
+ */
@@ -0,0 +1,59 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import React from 'react';
3
+
4
+ import '@testing-library/jest-dom';
5
+ import { PageProvider } from './PageProvider';
6
+
7
+ import { PageContext } from '../../contexts/PageContext';
8
+
9
+ const MockChildComponent = () => {
10
+ const context = React.useContext(PageContext);
11
+
12
+ return <div data-testid="mockChild">{JSON.stringify(context?.pageAsset.page.title)}</div>;
13
+ };
14
+
15
+ describe('PageProvider', () => {
16
+ const mockEntity = {
17
+ pageAsset: {
18
+ page: {
19
+ title: 'Test Page',
20
+ identifier: 'test-page-1'
21
+ }
22
+ // ... add other context properties as needed
23
+ }
24
+ };
25
+
26
+ it('provides the context to its children', () => {
27
+ render(
28
+ <PageProvider pageContext={mockEntity}>
29
+ <MockChildComponent />
30
+ </PageProvider>
31
+ );
32
+ expect(screen.getByTestId('mockChild')).toHaveTextContent(mockEntity.pageAsset.page.title);
33
+ });
34
+
35
+ it('updates context when entity changes', () => {
36
+ const { rerender } = render(
37
+ <PageProvider pageContext={mockEntity}>
38
+ <MockChildComponent />
39
+ </PageProvider>
40
+ );
41
+ // Change the context
42
+ const newEntity = {
43
+ ...mockEntity,
44
+ pageAsset: {
45
+ ...mockEntity.pageAsset,
46
+ page: {
47
+ ...mockEntity.pageAsset.page,
48
+ title: 'Updated Test Page'
49
+ }
50
+ }
51
+ };
52
+ rerender(
53
+ <PageProvider pageContext={newEntity}>
54
+ <MockChildComponent />
55
+ </PageProvider>
56
+ );
57
+ expect(screen.getByTestId('mockChild')).toHaveTextContent(newEntity.pageAsset.page.title);
58
+ });
59
+ });
@@ -0,0 +1,23 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { PageContext } from '../../contexts/PageContext';
4
+
5
+ export interface PageProviderProps {
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ readonly pageContext: any;
8
+ readonly children: ReactNode;
9
+ }
10
+
11
+ /**
12
+ * `PageProvider` is a functional component that provides a context for a DotCMS page.
13
+ * It takes a `PageProviderProps` object as a parameter and returns a JSX element.
14
+ *
15
+ * @category Components
16
+ * @param {PageProviderProps} props - The properties for the PageProvider. Includes an `entity` and `children`.
17
+ * @returns {JSX.Element} - A JSX element that provides a context for a DotCMS page.
18
+ */
19
+ export function PageProvider(props: PageProviderProps): JSX.Element {
20
+ const { pageContext, children } = props;
21
+
22
+ return <PageContext.Provider value={pageContext}>{children}</PageContext.Provider>;
23
+ }
@@ -0,0 +1,5 @@
1
+ .row {
2
+ display: grid;
3
+ grid-template-columns: repeat(12, 1fr);
4
+ gap: 1rem;
5
+ }