@eeacms/volto-group-block 6.2.0 → 6.3.0

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [6.3.0](https://github.com/eea/volto-group-block/compare/6.2.1...6.3.0) - 5 September 2023
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(group): Variations support refs #157040 #26 from eea/variationsSupport [ichim-david - [`8342ae8`](https://github.com/eea/volto-group-block/commit/8342ae8d843bd672339f9472e1e806cbfeac8d1b)]
12
+
13
+ #### :bug: Bug Fixes
14
+
15
+ - fix: update and fix jest tests [nileshgulia1 - [`97df2cc`](https://github.com/eea/volto-group-block/commit/97df2cc6ac5c3283707ff72bb3d4cd762d5a3b2f)]
16
+
17
+ #### :hammer_and_wrench: Others
18
+
19
+ - Release 6.3.0 [Alin Voinea - [`3d74bb5`](https://github.com/eea/volto-group-block/commit/3d74bb5342e3882e0ddee1aa8edab7e90949def6)]
20
+ - i18n: Add en [Alin Voinea - [`f3385cd`](https://github.com/eea/volto-group-block/commit/f3385cd33bfbe3efe514fd82fd140d17e33051e1)]
21
+ - test: Update Makefile and docker-compose to align it with Jenkinsfile [valentinab25 - [`3aa996b`](https://github.com/eea/volto-group-block/commit/3aa996b4c115da6f37ca771f07f10d58fbfa33e8)]
22
+ - fix eslint warnings [nileshgulia1 - [`cfdf2e9`](https://github.com/eea/volto-group-block/commit/cfdf2e900bcc456fa5a24ce7b03859170ad024ba)]
23
+ - feature: add variations support [nileshgulia1 - [`d981c6b`](https://github.com/eea/volto-group-block/commit/d981c6b59c669712d60feb9cfb04022e228ac001)]
24
+ ### [6.2.1](https://github.com/eea/volto-group-block/compare/6.2.0...6.2.1) - 18 August 2023
25
+
26
+ #### :bug: Bug Fixes
27
+
28
+ - fix: ignoreSpaces on char counter - refs #256525 [rexalex - [`8d1ad2c`](https://github.com/eea/volto-group-block/commit/8d1ad2cfdf972b88f02f9efc4b7ef55b1e4b3592)]
29
+
30
+ #### :house: Documentation changes
31
+
32
+ - docs: Cleanup Makefile, update DEVELOP documentation, i18n - refs #254894 [valentinab25 - [`94b58f7`](https://github.com/eea/volto-group-block/commit/94b58f7c0390bfcac24adbfbea61577137c91e2b)]
33
+
34
+ #### :hammer_and_wrench: Others
35
+
36
+ - test: increase test coverage - refs #254313 [ana-oprea - [`5facade`](https://github.com/eea/volto-group-block/commit/5facadec2f866a066ca836f8247a5e58ea44d176)]
7
37
  ### [6.2.0](https://github.com/eea/volto-group-block/compare/6.1.2...6.2.0) - 20 July 2023
8
38
 
9
39
  #### :nail_care: Enhancements
package/DEVELOP.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Develop
4
4
 
5
+ 1. Make sure you have `docker` and `docker compose` installed and running on your machine:
6
+
7
+ ```Bash
8
+ git clone https://github.com/eea/volto-group-block.git
9
+ cd volto-group-block
10
+ git checkout -b bugfix-123456 develop
11
+ make
12
+ make start
13
+ ```
14
+
15
+ 1. Wait for `Volto started at 0.0.0.0:3000` meesage
16
+
17
+ 1. Go to http://localhost:3000
18
+
19
+ 1. Happy hacking!
20
+
21
+ ```Bash
22
+ cd src/addons/volto-group-block/
23
+ ```
24
+
25
+ ### Or add @eeacms/volto-group-block to your Volto project
26
+
5
27
  Before starting make sure your development environment is properly set. See [Volto Developer Documentation](https://docs.voltocms.com/getting-started/install/)
6
28
 
7
29
  1. Make sure you have installed `yo`, `@plone/generator-volto` and `mrs-developer`
@@ -48,3 +70,37 @@ Before starting make sure your development environment is properly set. See [Vol
48
70
  1. Happy hacking!
49
71
 
50
72
  cd src/addons/volto-group-block/
73
+
74
+ ## Cypress
75
+
76
+ To run cypress locally, first make sure you don't have any Volto/Plone running on ports `8080` and `3000`.
77
+
78
+ You don't have to be in a `clean-volto-project`, you can be in any Volto Frontend
79
+ project where you added `volto-group-block` to `mrs.developer.json`
80
+
81
+ Go to:
82
+
83
+ ```BASH
84
+ cd src/addons/volto-group-block/
85
+ ```
86
+
87
+ Start:
88
+
89
+ ```Bash
90
+ make
91
+ make start
92
+ ```
93
+
94
+ This will build and start with Docker a clean `Plone backend` and `Volto Frontend` with `volto-group-block` block installed.
95
+
96
+ Open Cypress Interface:
97
+
98
+ ```Bash
99
+ make cypress-open
100
+ ```
101
+
102
+ Or run it:
103
+
104
+ ```Bash
105
+ make cypress-run
106
+ ```
package/README.md CHANGED
@@ -51,6 +51,10 @@
51
51
 
52
52
  1. Make sure you have a [Plone backend](https://plone.org/download) up-and-running at http://localhost:8080/Plone
53
53
 
54
+ ```Bash
55
+ docker compose up backend
56
+ ```
57
+
54
58
  1. Start Volto frontend
55
59
 
56
60
  - If you already have a volto project, just update `package.json`:
@@ -0,0 +1,32 @@
1
+ version: "3"
2
+ services:
3
+ backend:
4
+ image: eeacms/plone-backend
5
+ ports:
6
+ - "8080:8080"
7
+ environment:
8
+ SITE: "Plone"
9
+ PROFILES: "eea.kitkat:testing"
10
+
11
+ frontend:
12
+ build:
13
+ context: ./
14
+ dockerfile: ./Dockerfile
15
+ args:
16
+ ADDON_NAME: "${ADDON_NAME}"
17
+ ADDON_PATH: "${ADDON_PATH}"
18
+ VOLTO_VERSION: ${VOLTO_VERSION:-16}
19
+ ports:
20
+ - "3000:3000"
21
+ - "3001:3001"
22
+ depends_on:
23
+ - backend
24
+ volumes:
25
+ - ./:/app/src/addons/${ADDON_PATH}
26
+ environment:
27
+ CI: "true"
28
+ NODE_ENV: "development"
29
+ RAZZLE_JEST_CONFIG: "src/addons/${ADDON_PATH}/jest-addon.config.js"
30
+ RAZZLE_INTERNAL_API_PATH: "http://backend:8080/Plone"
31
+ RAZZLE_DEV_PROXY_API_PATH: "http://backend:8080/Plone"
32
+ HOST: "0.0.0.0"
@@ -0,0 +1,22 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: \n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: \n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: \n"
8
+ "Language: \n"
9
+ "Language-Team: \n"
10
+ "Content-Type: \n"
11
+ "Content-Transfer-Encoding: \n"
12
+ "Plural-Forms: \n"
13
+
14
+ #: components/manage/Blocks/Group/EditBlockWrapper
15
+ # defaultMessage: Unknown Block {block}
16
+ msgid "Unknown Block"
17
+ msgstr ""
18
+
19
+ #: components/manage/Blocks/Group/EditBlockWrapper
20
+ # defaultMessage: delete
21
+ msgid "delete"
22
+ msgstr ""
@@ -0,0 +1,22 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: \n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: \n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: \n"
8
+ "Language: \n"
9
+ "Language-Team: \n"
10
+ "Content-Type: \n"
11
+ "Content-Transfer-Encoding: \n"
12
+ "Plural-Forms: \n"
13
+
14
+ #: components/manage/Blocks/Group/EditBlockWrapper
15
+ # defaultMessage: Unknown Block {block}
16
+ msgid "Unknown Block"
17
+ msgstr ""
18
+
19
+ #: components/manage/Blocks/Group/EditBlockWrapper
20
+ # defaultMessage: delete
21
+ msgid "delete"
22
+ msgstr ""
@@ -0,0 +1,22 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: \n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: \n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: \n"
8
+ "Language: \n"
9
+ "Language-Team: \n"
10
+ "Content-Type: \n"
11
+ "Content-Transfer-Encoding: \n"
12
+ "Plural-Forms: \n"
13
+
14
+ #: components/manage/Blocks/Group/EditBlockWrapper
15
+ # defaultMessage: Unknown Block {block}
16
+ msgid "Unknown Block"
17
+ msgstr ""
18
+
19
+ #: components/manage/Blocks/Group/EditBlockWrapper
20
+ # defaultMessage: delete
21
+ msgid "delete"
22
+ msgstr ""
@@ -0,0 +1,22 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: \n"
4
+ "Report-Msgid-Bugs-To: \n"
5
+ "POT-Creation-Date: \n"
6
+ "PO-Revision-Date: \n"
7
+ "Last-Translator: \n"
8
+ "Language: \n"
9
+ "Language-Team: \n"
10
+ "Content-Type: \n"
11
+ "Content-Transfer-Encoding: \n"
12
+ "Plural-Forms: \n"
13
+
14
+ #: components/manage/Blocks/Group/EditBlockWrapper
15
+ # defaultMessage: Unknown Block {block}
16
+ msgid "Unknown Block"
17
+ msgstr ""
18
+
19
+ #: components/manage/Blocks/Group/EditBlockWrapper
20
+ # defaultMessage: delete
21
+ msgid "delete"
22
+ msgstr ""
package/locales/volto.pot CHANGED
@@ -0,0 +1,24 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: Plone\n"
4
+ "POT-Creation-Date: 2023-08-29T17:38:01.779Z\n"
5
+ "Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
6
+ "Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
7
+ "MIME-Version: 1.0\n"
8
+ "Content-Type: text/plain; charset=utf-8\n"
9
+ "Content-Transfer-Encoding: 8bit\n"
10
+ "Plural-Forms: nplurals=1; plural=0;\n"
11
+ "Language-Code: en\n"
12
+ "Language-Name: English\n"
13
+ "Preferred-Encodings: utf-8\n"
14
+ "Domain: volto\n"
15
+
16
+ #: components/manage/Blocks/Group/EditBlockWrapper
17
+ # defaultMessage: Unknown Block {block}
18
+ msgid "Unknown Block"
19
+ msgstr ""
20
+
21
+ #: components/manage/Blocks/Group/EditBlockWrapper
22
+ # defaultMessage: delete
23
+ msgid "delete"
24
+ msgstr ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-group-block",
3
- "version": "6.2.0",
3
+ "version": "6.3.0",
4
4
  "description": "volto-group-block: Volto block to be used to group other blocks",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -1,3 +1,4 @@
1
- export GroupBlockEdit from './manage/Blocks/Group/Edit';
2
- export GroupBlockView from './manage/Blocks/Group/View';
3
- export GroupBlockLayout from './manage/Blocks/Group/LayoutSchema';
1
+ export { default as GroupBlockDefaultBody } from './manage/Blocks/Group/DefaultBody';
2
+ export { default as GroupBlockEdit } from './manage/Blocks/Group/Edit';
3
+ export { default as GroupBlockView } from './manage/Blocks/Group/View';
4
+ export { default as GroupBlockLayout } from './manage/Blocks/Group/LayoutSchema';
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { GroupBlockDefaultBody } from '@eeacms/volto-group-block/components';
4
+
5
+ const Body = (props) => {
6
+ const { variation } = props;
7
+
8
+ const BodyComponent = variation?.template || GroupBlockDefaultBody;
9
+
10
+ return <BodyComponent {...props} />;
11
+ };
12
+
13
+ Body.propTypes = {
14
+ variation: PropTypes.objectOf(PropTypes.any).isRequired,
15
+ };
16
+
17
+ export default Body;
@@ -8,46 +8,53 @@ import { serializeNodesToText } from '@plone/volto-slate/editor/render';
8
8
  import delightedSVG from '@plone/volto/icons/delighted.svg';
9
9
  import dissatisfiedSVG from '@plone/volto/icons/dissatisfied.svg';
10
10
 
11
- const CounterComponent = ({ data, setSidebarTab, setSelectedBlock }) => {
12
- const { maxChars } = data;
13
- let charCount = 0;
14
-
15
- const countCharsWithoutSpaces = (paragraph) => {
16
- const regex = /[^\s\\]/g;
11
+ const countCharsWithoutSpaces = (paragraph) => {
12
+ const regex = /[^\s\\]/g;
13
+ return (paragraph.match(regex) || []).length;
14
+ };
17
15
 
18
- return (paragraph.match(regex) || []).length;
19
- };
16
+ const countCharsWithSpaces = (paragraph) => {
17
+ return paragraph?.length || 0;
18
+ };
20
19
 
21
- const countCharsWithSpaces = (paragraph) => {
22
- return paragraph?.length || 0;
23
- };
20
+ const countTextInEachBlock = (countTextIn, ignoreSpaces, groupCharCount) => ([
21
+ id,
22
+ blockData,
23
+ ]) => {
24
+ const foundText =
25
+ blockData && countTextIn?.includes(blockData?.['@type'])
26
+ ? isString(blockData?.plaintext)
27
+ ? blockData?.plaintext
28
+ : isArray(blockData?.value) && blockData?.value !== null
29
+ ? serializeNodesToText(blockData?.value)
30
+ : ''
31
+ : '';
24
32
 
25
- const countTextInBlocks = (blocksObject) => {
26
- const { countTextIn } = config.blocks?.blocksConfig?.group;
27
- let groupCharCount = 0;
28
- if (!maxChars) {
29
- return groupCharCount;
30
- }
31
- if (!blocksObject) return groupCharCount;
33
+ groupCharCount.value += ignoreSpaces
34
+ ? countCharsWithoutSpaces(foundText)
35
+ : countCharsWithSpaces(foundText);
36
+ };
32
37
 
33
- visitBlocks(blocksObject, ([id, data]) => {
34
- let foundText;
35
- if (data && countTextIn?.includes(data?.['@type'])) {
36
- if (isString(data?.plaintext)) foundText = data?.plaintext;
37
- else if (isArray(data?.value) && data?.value !== null)
38
- foundText = serializeNodesToText(data?.value);
39
- } else foundText = '';
38
+ const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
39
+ const { countTextIn } = config.blocks?.blocksConfig?.group;
40
+ // use obj ref to update value - if you send number it will not be updated
41
+ const groupCharCount = { value: 0 };
40
42
 
41
- groupCharCount += data?.ignoreSpaces
42
- ? countCharsWithoutSpaces(foundText)
43
- : countCharsWithSpaces(foundText);
44
- });
43
+ if (!maxChars || !blocksObject) {
44
+ return groupCharCount.value;
45
+ }
45
46
 
46
- return groupCharCount;
47
- };
47
+ visitBlocks(
48
+ blocksObject,
49
+ countTextInEachBlock(countTextIn, ignoreSpaces, groupCharCount),
50
+ );
48
51
 
49
- charCount = countTextInBlocks(data?.data);
52
+ return groupCharCount.value;
53
+ };
50
54
 
55
+ const CounterComponent = ({ data, setSidebarTab, setSelectedBlock }) => {
56
+ const { maxChars, ignoreSpaces } = data;
57
+ const charCount = countTextInBlocks(data?.data, ignoreSpaces, maxChars);
51
58
  const counterClass =
52
59
  charCount < Math.ceil(maxChars / 1.05)
53
60
  ? 'info'
@@ -0,0 +1,234 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import CounterComponent from './CounterComponent';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+
6
+ jest.mock('@plone/volto/registry', () => ({
7
+ blocks: {
8
+ blocksConfig: {
9
+ group: {
10
+ countTextIn: ['text'],
11
+ },
12
+ },
13
+ },
14
+ }));
15
+
16
+ jest.mock('@plone/volto-slate/editor/render', () => ({
17
+ serializeNodesToText: jest.fn((nodes) =>
18
+ nodes.map((node) => node.text).join(' '),
19
+ ),
20
+ }));
21
+
22
+ describe('CounterComponent', () => {
23
+ const setSidebarTab = jest.fn();
24
+ const setSelectedBlock = jest.fn();
25
+
26
+ it('should render info class when character count is less than 95% of maxChars', () => {
27
+ const { container, getByText } = render(
28
+ <CounterComponent
29
+ data={{
30
+ maxChars: 100,
31
+ data: {
32
+ blocks: {
33
+ block1: { '@type': 'text', plaintext: 'test' },
34
+ },
35
+ blocks_layout: {
36
+ items: ['block1'],
37
+ },
38
+ },
39
+ }}
40
+ setSidebarTab={setSidebarTab}
41
+ setSelectedBlock={setSelectedBlock}
42
+ />,
43
+ );
44
+ expect(getByText('96 characters remaining out of 100')).toBeInTheDocument();
45
+ expect(container.querySelector('.counter.info')).toBeInTheDocument();
46
+ });
47
+
48
+ it('should render warning class when character count is between 95% and 100% of maxChars', () => {
49
+ const { container, getByText } = render(
50
+ <CounterComponent
51
+ data={{
52
+ maxChars: 100,
53
+ data: {
54
+ blocks: {
55
+ block1: { '@type': 'text', plaintext: 'test'.repeat(24) },
56
+ },
57
+ blocks_layout: {
58
+ items: ['block1'],
59
+ },
60
+ },
61
+ }}
62
+ setSidebarTab={setSidebarTab}
63
+ setSelectedBlock={setSelectedBlock}
64
+ />,
65
+ );
66
+ expect(getByText('4 characters remaining out of 100')).toBeInTheDocument();
67
+ expect(container.querySelector('.counter.warning')).toBeInTheDocument();
68
+ });
69
+
70
+ it('should render warning class when character count is over the maxChars', () => {
71
+ const { container, getByText } = render(
72
+ <CounterComponent
73
+ data={{
74
+ maxChars: 100,
75
+ data: {
76
+ blocks: {
77
+ block1: { '@type': 'text', plaintext: 'test'.repeat(26) },
78
+ },
79
+ blocks_layout: {
80
+ items: ['block1'],
81
+ },
82
+ },
83
+ }}
84
+ setSidebarTab={setSidebarTab}
85
+ setSelectedBlock={setSelectedBlock}
86
+ />,
87
+ );
88
+ expect(getByText('4 characters over the limit')).toBeInTheDocument();
89
+ expect(container.querySelector('.counter.danger')).toBeInTheDocument();
90
+ });
91
+
92
+ it('should handle click event', () => {
93
+ const { container } = render(
94
+ <CounterComponent
95
+ data={{
96
+ maxChars: 100,
97
+ data: {
98
+ blocks: {
99
+ block1: {
100
+ '@type': 'text',
101
+ plaintext: 'test'.repeat(24),
102
+ ignoreSpaces: true,
103
+ },
104
+ },
105
+ blocks_layout: {
106
+ items: ['block1'],
107
+ },
108
+ },
109
+ }}
110
+ setSidebarTab={setSidebarTab}
111
+ setSelectedBlock={setSelectedBlock}
112
+ />,
113
+ );
114
+ container.querySelector('.counter').click();
115
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
116
+ expect(setSelectedBlock).toHaveBeenCalled();
117
+ });
118
+
119
+ it('should handle click event with maxChar undefined', () => {
120
+ const { container } = render(
121
+ <CounterComponent
122
+ data={{
123
+ maxChars: undefined,
124
+ data: undefined,
125
+ }}
126
+ setSidebarTab={setSidebarTab}
127
+ setSelectedBlock={setSelectedBlock}
128
+ />,
129
+ );
130
+ container.querySelector('.counter').click();
131
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
132
+ expect(setSelectedBlock).toHaveBeenCalled();
133
+ });
134
+
135
+ it('should handle click event with data undefined', () => {
136
+ const { container } = render(
137
+ <CounterComponent
138
+ data={{
139
+ maxChars: 100,
140
+ data: undefined,
141
+ }}
142
+ setSidebarTab={setSidebarTab}
143
+ setSelectedBlock={setSelectedBlock}
144
+ />,
145
+ );
146
+ container.querySelector('.counter').click();
147
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
148
+ expect(setSelectedBlock).toHaveBeenCalled();
149
+ });
150
+
151
+ it('should handle click event with plaintext undefined, but values present', () => {
152
+ const { container } = render(
153
+ <CounterComponent
154
+ data={{
155
+ maxChars: 100,
156
+ data: {
157
+ blocks: {
158
+ block1: {
159
+ '@type': 'text',
160
+ plaintext: undefined,
161
+ value: [
162
+ { text: 'test' },
163
+ { children: [{ text: 'more text' }] },
164
+ ],
165
+ },
166
+ },
167
+ blocks_layout: {
168
+ items: ['block1'],
169
+ },
170
+ },
171
+ }}
172
+ setSidebarTab={setSidebarTab}
173
+ setSelectedBlock={setSelectedBlock}
174
+ />,
175
+ );
176
+ container.querySelector('.counter').click();
177
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
178
+ expect(setSelectedBlock).toHaveBeenCalled();
179
+ });
180
+
181
+ it('should handle click event with plaintext undefined and values is not an array and the type is in countTextIn', () => {
182
+ const { container } = render(
183
+ <CounterComponent
184
+ data={{
185
+ maxChars: 100,
186
+ data: {
187
+ blocks: {
188
+ block1: {
189
+ '@type': 'text',
190
+ plaintext: undefined,
191
+ value: {},
192
+ },
193
+ },
194
+ blocks_layout: {
195
+ items: ['block1'],
196
+ },
197
+ },
198
+ }}
199
+ setSidebarTab={setSidebarTab}
200
+ setSelectedBlock={setSelectedBlock}
201
+ />,
202
+ );
203
+ container.querySelector('.counter').click();
204
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
205
+ expect(setSelectedBlock).toHaveBeenCalled();
206
+ });
207
+
208
+ it('should handle click event with plaintext undefined and values is not an array and the type is not in countTextIn', () => {
209
+ const { container } = render(
210
+ <CounterComponent
211
+ data={{
212
+ maxChars: 100,
213
+ data: {
214
+ blocks: {
215
+ block1: {
216
+ '@type': 'test',
217
+ plaintext: undefined,
218
+ value: {},
219
+ },
220
+ },
221
+ blocks_layout: {
222
+ items: ['block1'],
223
+ },
224
+ },
225
+ }}
226
+ setSidebarTab={setSidebarTab}
227
+ setSelectedBlock={setSelectedBlock}
228
+ />,
229
+ );
230
+ container.querySelector('.counter').click();
231
+ expect(setSidebarTab).toHaveBeenCalledWith(1);
232
+ expect(setSelectedBlock).toHaveBeenCalled();
233
+ });
234
+ });
@@ -0,0 +1,105 @@
1
+ import { Button } from 'semantic-ui-react';
2
+ import { BlocksForm, Icon, RenderBlocks } from '@plone/volto/components';
3
+ import EditBlockWrapper from './EditBlockWrapper';
4
+
5
+ import helpSVG from '@plone/volto/icons/help.svg';
6
+
7
+ const GroupBlockDefaultBody = (props) => {
8
+ const {
9
+ block,
10
+ data,
11
+ onChangeBlock,
12
+ onChangeField,
13
+ pathname,
14
+ selected,
15
+ selectedBlock,
16
+ onSelectBlock,
17
+ setSelectedBlock,
18
+ manage,
19
+ childBlocksForm,
20
+ multiSelected,
21
+ formDescription,
22
+ isEditMode,
23
+ } = props;
24
+ const metadata = props.metadata || props.properties;
25
+ const blockState = {};
26
+
27
+ // Get editing instructions from block settings or props
28
+ let instructions = data?.instructions?.data || data?.instructions;
29
+ if (!instructions || instructions === '<p><br/></p>') {
30
+ instructions = formDescription;
31
+ }
32
+ return isEditMode ? (
33
+ <BlocksForm
34
+ metadata={metadata}
35
+ properties={childBlocksForm}
36
+ manage={manage}
37
+ selectedBlock={selected ? selectedBlock : null}
38
+ allowedBlocks={data.allowedBlocks}
39
+ title={data.placeholder}
40
+ description={instructions}
41
+ onSelectBlock={(id, l, e) => {
42
+ const isMultipleSelection = e
43
+ ? e.shiftKey || e.ctrlKey || e.metaKey
44
+ : false;
45
+ onSelectBlock(id, isMultipleSelection, e, selectedBlock);
46
+ }}
47
+ onChangeFormData={(newFormData) => {
48
+ onChangeBlock(block, {
49
+ ...data,
50
+ data: newFormData,
51
+ });
52
+ }}
53
+ onChangeField={(id, value) => {
54
+ if (['blocks', 'blocks_layout'].indexOf(id) > -1) {
55
+ blockState[id] = value;
56
+ onChangeBlock(block, {
57
+ ...data,
58
+ data: {
59
+ ...data.data,
60
+ ...blockState,
61
+ },
62
+ });
63
+ } else {
64
+ onChangeField(id, value);
65
+ }
66
+ }}
67
+ pathname={pathname}
68
+ >
69
+ {({ draginfo }, editBlock, blockProps) => (
70
+ <EditBlockWrapper
71
+ draginfo={draginfo}
72
+ blockProps={blockProps}
73
+ disabled={data.disableInnerButtons}
74
+ extraControls={
75
+ <>
76
+ {instructions && (
77
+ <>
78
+ <Button
79
+ icon
80
+ basic
81
+ title="Section help"
82
+ onClick={() => {
83
+ setSelectedBlock();
84
+ const tab = manage ? 0 : 1;
85
+ props.setSidebarTab(tab);
86
+ }}
87
+ >
88
+ <Icon name={helpSVG} className="" size="19px" />
89
+ </Button>
90
+ </>
91
+ )}
92
+ </>
93
+ }
94
+ multiSelected={multiSelected.includes(blockProps.block)}
95
+ >
96
+ {editBlock}
97
+ </EditBlockWrapper>
98
+ )}
99
+ </BlocksForm>
100
+ ) : (
101
+ <RenderBlocks {...props} metadata={metadata} content={data?.data || {}} />
102
+ );
103
+ };
104
+
105
+ export default GroupBlockDefaultBody;
@@ -1,46 +1,35 @@
1
1
  import React, { useState, useCallback } from 'react';
2
2
  import { isEmpty, without } from 'lodash';
3
+ import {
4
+ emptyBlocksForm,
5
+ withBlockExtensions,
6
+ getBlocksLayoutFieldname,
7
+ } from '@plone/volto/helpers';
8
+ import BodyComponent from './Body';
9
+
3
10
  import config from '@plone/volto/registry';
4
11
  import {
5
- BlocksForm,
6
12
  SidebarPortal,
7
- Icon,
8
13
  BlockDataForm,
9
14
  BlocksToolbar,
10
15
  } from '@plone/volto/components';
11
- import {
12
- emptyBlocksForm,
13
- getBlocksLayoutFieldname,
14
- } from '@plone/volto/helpers';
15
16
  import PropTypes from 'prop-types';
16
- import { Button, Segment } from 'semantic-ui-react';
17
- import EditBlockWrapper from './EditBlockWrapper';
17
+ import { Segment } from 'semantic-ui-react';
18
18
  import EditSchema from './EditSchema';
19
+
19
20
  import CounterComponent from './CounterComponent';
20
- import helpSVG from '@plone/volto/icons/help.svg';
21
21
  import './editor.less';
22
22
 
23
23
  const Edit = (props) => {
24
- const {
25
- block,
26
- data,
27
- onChangeBlock,
28
- onChangeField,
29
- pathname,
30
- selected,
31
- manage,
32
- formDescription,
33
- } = props;
34
- const metadata = props.metadata || props.properties;
24
+ const { block, data, onChangeBlock, selected, formDescription } = props;
35
25
  const [multiSelected, setMultiSelected] = useState([]);
36
26
  const data_blocks = data?.data?.blocks;
37
- const properties = isEmpty(data_blocks) ? emptyBlocksForm() : data.data;
27
+ const childBlocksForm = isEmpty(data_blocks) ? emptyBlocksForm() : data.data;
38
28
 
39
29
  const [selectedBlock, setSelectedBlock] = useState(
40
- properties.blocks_layout.items[0],
30
+ childBlocksForm.blocks_layout.items[0],
41
31
  );
42
32
 
43
- const blockState = {};
44
33
  const handleKeyDown = (
45
34
  e,
46
35
  index,
@@ -131,15 +120,15 @@ const Edit = (props) => {
131
120
  React.useEffect(() => {
132
121
  if (
133
122
  isEmpty(data_blocks) &&
134
- properties.blocks_layout.items[0] !== selectedBlock
123
+ childBlocksForm.blocks_layout.items[0] !== selectedBlock
135
124
  ) {
136
- setSelectedBlock(properties.blocks_layout.items[0]);
125
+ setSelectedBlock(childBlocksForm.blocks_layout.items[0]);
137
126
  onChangeBlock(block, {
138
127
  ...data,
139
- data: properties,
128
+ data: childBlocksForm,
140
129
  });
141
130
  }
142
- }, [onChangeBlock, properties, selectedBlock, block, data, data_blocks]);
131
+ }, [onChangeBlock, childBlocksForm, selectedBlock, block, data, data_blocks]);
143
132
 
144
133
  // Get editing instructions from block settings or props
145
134
  let instructions = data?.instructions?.data || data?.instructions;
@@ -167,6 +156,16 @@ const Edit = (props) => {
167
156
  >
168
157
  {data.title || 'Section'}
169
158
  </legend>
159
+ <BodyComponent
160
+ {...props}
161
+ isEditMode={true}
162
+ selectedBlock={selectedBlock}
163
+ setSelectedBlock={setSelectedBlock}
164
+ multiSelected={multiSelected}
165
+ setMultiSelected={setMultiSelected}
166
+ onSelectBlock={onSelectBlock}
167
+ childBlocksForm={childBlocksForm}
168
+ />
170
169
  {selected ? (
171
170
  <BlocksToolbar
172
171
  selectedBlock={Object.keys(selectedBlock || {})[0]}
@@ -189,74 +188,6 @@ const Edit = (props) => {
189
188
  ) : (
190
189
  ''
191
190
  )}
192
- <BlocksForm
193
- metadata={metadata}
194
- properties={properties}
195
- manage={manage}
196
- selectedBlock={selected ? selectedBlock : null}
197
- allowedBlocks={data.allowedBlocks}
198
- title={data.placeholder}
199
- description={instructions}
200
- onSelectBlock={(id, l, e) => {
201
- const isMultipleSelection = e
202
- ? e.shiftKey || e.ctrlKey || e.metaKey
203
- : false;
204
- onSelectBlock(id, isMultipleSelection, e, selectedBlock);
205
- }}
206
- onChangeFormData={(newFormData) => {
207
- onChangeBlock(block, {
208
- ...data,
209
- data: newFormData,
210
- });
211
- }}
212
- onChangeField={(id, value) => {
213
- if (['blocks', 'blocks_layout'].indexOf(id) > -1) {
214
- blockState[id] = value;
215
- onChangeBlock(block, {
216
- ...data,
217
- data: {
218
- ...data.data,
219
- ...blockState,
220
- },
221
- });
222
- } else {
223
- onChangeField(id, value);
224
- }
225
- }}
226
- pathname={pathname}
227
- >
228
- {({ draginfo }, editBlock, blockProps) => (
229
- <EditBlockWrapper
230
- draginfo={draginfo}
231
- blockProps={blockProps}
232
- disabled={data.disableInnerButtons}
233
- extraControls={
234
- <>
235
- {instructions && (
236
- <>
237
- <Button
238
- icon
239
- basic
240
- title="Section help"
241
- onClick={() => {
242
- setSelectedBlock();
243
- const tab = manage ? 0 : 1;
244
- props.setSidebarTab(tab);
245
- }}
246
- >
247
- <Icon name={helpSVG} className="" size="19px" />
248
- </Button>
249
- </>
250
- )}
251
- </>
252
- }
253
- multiSelected={multiSelected.includes(blockProps.block)}
254
- >
255
- {editBlock}
256
- </EditBlockWrapper>
257
- )}
258
- </BlocksForm>
259
-
260
191
  {props.data.maxChars && (
261
192
  <CounterComponent {...props} setSelectedBlock={setSelectedBlock} />
262
193
  )}
@@ -293,4 +224,4 @@ Edit.propTypes = {
293
224
  manage: PropTypes.bool.isRequired,
294
225
  };
295
226
 
296
- export default Edit;
227
+ export default withBlockExtensions(Edit);
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import Edit from './Edit';
2
+ import { default as Edit } from './Edit';
3
3
  import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
5
  import thunk from 'redux-thunk';
@@ -15,6 +15,21 @@ const store = mockStore({
15
15
  },
16
16
  });
17
17
 
18
+ jest.mock('@plone/volto/components', () => ({
19
+ BlocksForm: jest.fn(() => <div className="blocks-form">RenderBlocks</div>),
20
+ Icon: () => <div>Icon</div>,
21
+ SidebarPortal: () => <div>SidebarPortal</div>,
22
+ BlocksToolbar: () => <div>BlocksToolbar</div>,
23
+ BlockDataForm: () => <div>BlockDataForm</div>,
24
+ RenderBlocks: jest.fn(() => <div>RenderBlocks</div>),
25
+ }));
26
+
27
+ jest.mock('@plone/volto/helpers', () => ({
28
+ withBlockExtensions: jest.fn((Component) => Component),
29
+ emptyBlocksForm: jest.fn(),
30
+ getBlocksLayoutFieldname: jest.fn(),
31
+ }));
32
+
18
33
  describe('Edit', () => {
19
34
  const onChangeBlock = jest.fn();
20
35
  const onChangeField = jest.fn();
@@ -41,6 +56,7 @@ describe('Edit', () => {
41
56
  pathname: '/',
42
57
  selected: true,
43
58
  manage: true,
59
+ variation: {},
44
60
  };
45
61
 
46
62
  it('should render without crashing', () => {
@@ -76,4 +92,113 @@ describe('Edit', () => {
76
92
  );
77
93
  fireEvent.keyDown(getByRole('presentation'), { key: 'ArrowUp', code: 38 });
78
94
  });
95
+
96
+ it('should call ArrowUp keydown', () => {
97
+ const props = {
98
+ block: 'testBlock',
99
+ data: {
100
+ instructions: 'test',
101
+ data: {
102
+ blocks: {
103
+ block1: {
104
+ type: 'test',
105
+ data: {
106
+ value: 'Test',
107
+ },
108
+ },
109
+ },
110
+ blocks_layout: {
111
+ items: [undefined],
112
+ },
113
+ },
114
+ },
115
+ onChangeBlock,
116
+ onChangeField,
117
+ pathname: '/',
118
+ selected: true,
119
+ manage: true,
120
+ variation: {},
121
+ };
122
+ const mockOnFocusPreviousBlock = jest.fn();
123
+ const mockOnFocusNextBlock = jest.fn();
124
+ const mockOnAddBlock = jest.fn();
125
+ const mockSidebarTab = jest.fn();
126
+
127
+ const { container } = render(
128
+ <Provider store={store}>
129
+ <Edit
130
+ {...props}
131
+ onFocusPreviousBlock={mockOnFocusPreviousBlock}
132
+ onFocusNextBlock={mockOnFocusNextBlock}
133
+ onAddBlock={mockOnAddBlock}
134
+ blockNode={mockBlockNode}
135
+ setSidebarTab={mockSidebarTab}
136
+ />
137
+ </Provider>,
138
+ );
139
+
140
+ fireEvent.keyDown(container.querySelector('.section-block'), {
141
+ key: 'ArrowUp',
142
+ code: 38,
143
+ });
144
+ fireEvent.keyDown(container.querySelector('.section-block'), {
145
+ key: 'ArrowDown',
146
+ code: 40,
147
+ });
148
+ fireEvent.keyDown(container.querySelector('.section-block'), {
149
+ key: 'Enter',
150
+ code: 13,
151
+ });
152
+
153
+ fireEvent.click(container.querySelector('.blocks-form'), {
154
+ shiftKey: true,
155
+ });
156
+ fireEvent.click(container.querySelector('.section-block legend'));
157
+ });
158
+
159
+ it('should call ArrowUp keydown', () => {
160
+ const props = {
161
+ block: 'testBlock',
162
+ data: {
163
+ instructions: 'test',
164
+ data: {
165
+ blocks: {
166
+ block1: {
167
+ type: 'test',
168
+ data: {
169
+ value: 'Test',
170
+ },
171
+ },
172
+ },
173
+ blocks_layout: {
174
+ items: [undefined],
175
+ },
176
+ },
177
+ },
178
+ onChangeBlock,
179
+ onChangeField,
180
+ pathname: '/',
181
+ selected: true,
182
+ manage: true,
183
+ variation: {},
184
+ };
185
+ const mockOnFocusPreviousBlock = jest.fn();
186
+ const mockOnFocusNextBlock = jest.fn();
187
+ const mockOnAddBlock = jest.fn();
188
+ const mockSidebarTab = jest.fn();
189
+ const { container } = render(
190
+ <Provider store={store}>
191
+ <Edit
192
+ {...props}
193
+ onFocusPreviousBlock={mockOnFocusPreviousBlock}
194
+ onFocusNextBlock={mockOnFocusNextBlock}
195
+ onAddBlock={mockOnAddBlock}
196
+ setSidebarTab={mockSidebarTab}
197
+ blockNode={mockBlockNode}
198
+ />
199
+ </Provider>,
200
+ );
201
+
202
+ fireEvent.click(container.querySelector('.section-block legend'));
203
+ });
79
204
  });
@@ -1,9 +1,9 @@
1
1
  import React from 'react';
2
- import { RenderBlocks } from '@plone/volto/components';
2
+ import { withBlockExtensions } from '@plone/volto/helpers';
3
+ import BodyComponent from './Body';
3
4
 
4
5
  const View = (props) => {
5
6
  const { data } = props;
6
- const metadata = props.metadata || props.properties;
7
7
  const CustomTag = `${data.as || 'div'}`;
8
8
  const customId = data?.title
9
9
  ?.toLowerCase()
@@ -12,9 +12,9 @@ const View = (props) => {
12
12
  ?.replace(/\s+/gi, '-');
13
13
  return (
14
14
  <CustomTag id={customId}>
15
- <RenderBlocks {...props} metadata={metadata} content={data?.data || {}} />
15
+ <BodyComponent {...props} />
16
16
  </CustomTag>
17
17
  );
18
18
  };
19
19
 
20
- export default View;
20
+ export default withBlockExtensions(View);
@@ -7,6 +7,11 @@ import '@testing-library/jest-dom/extend-expect';
7
7
 
8
8
  jest.mock('@plone/volto/components', () => ({
9
9
  RenderBlocks: jest.fn(() => <div>RenderBlocks</div>),
10
+ BodyComponent: () => <div>BodyComponent</div>,
11
+ }));
12
+
13
+ jest.mock('@plone/volto/helpers', () => ({
14
+ withBlockExtensions: jest.fn((Component) => Component),
10
15
  }));
11
16
 
12
17
  describe('View', () => {
@@ -15,6 +20,7 @@ describe('View', () => {
15
20
  data: {},
16
21
  metadata: {},
17
22
  properties: {},
23
+ variation: {},
18
24
  };
19
25
  const component = renderer.create(<View {...props} />);
20
26
 
@@ -24,9 +30,12 @@ describe('View', () => {
24
30
 
25
31
  it('renders with default tag and without crashing', () => {
26
32
  const props = {
27
- data: {},
33
+ data: {
34
+ variation: {},
35
+ },
28
36
  metadata: {},
29
37
  properties: {},
38
+ variation: {},
30
39
  };
31
40
  const { container } = render(<View {...props} />);
32
41
  expect(container.querySelector('div')).toBeInTheDocument();
@@ -40,6 +49,7 @@ describe('View', () => {
40
49
  data: { key: 'value' },
41
50
  },
42
51
  properties: {},
52
+ variation: {},
43
53
  };
44
54
  const { container } = render(<View {...props} />);
45
55
  expect(container.querySelector('section')).toBeInTheDocument();
@@ -55,6 +65,7 @@ describe('View', () => {
55
65
  },
56
66
  metadata: { meta: 'data' },
57
67
  properties: { prop: 'erty' },
68
+ variation: {},
58
69
  };
59
70
  render(<View {...props} />);
60
71
  expect(RenderBlocks).toHaveBeenCalledWith(
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { getBlocks } from '@plone/volto/helpers';
2
- import { GroupBlockEdit, GroupBlockView, GroupBlockLayout } from './components';
3
-
2
+ import {
3
+ GroupBlockEdit,
4
+ GroupBlockView,
5
+ GroupBlockLayout,
6
+ GroupBlockDefaultBody,
7
+ } from './components';
4
8
  import codeSVG from '@plone/volto/icons/row.svg';
5
9
 
6
10
  const applyConfig = (config) => {
@@ -45,6 +49,14 @@ const applyConfig = (config) => {
45
49
  addPermission: [],
46
50
  view: [],
47
51
  },
52
+ variations: [
53
+ {
54
+ id: 'default',
55
+ isDefault: true,
56
+ title: 'Default',
57
+ template: GroupBlockDefaultBody,
58
+ },
59
+ ],
48
60
  tocEntries: (block = {}, tocData) => {
49
61
  // integration with volto-block-toc
50
62
  const headlines = tocData.levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
@@ -0,0 +1,101 @@
1
+ import applyConfig from './index';
2
+
3
+ describe('applyConfig', () => {
4
+ it('should add group block configuration', () => {
5
+ const config = {
6
+ blocks: {
7
+ blocksConfig: {
8
+ text: { title: 'Text', restricted: false },
9
+ image: { title: 'Image', restricted: true },
10
+ },
11
+ },
12
+ };
13
+
14
+ const newConfig = applyConfig(config);
15
+
16
+ expect(newConfig.blocks.blocksConfig.group).toBeDefined();
17
+ expect(newConfig.blocks.blocksConfig.group.id).toEqual('group');
18
+ expect(newConfig.blocks.blocksConfig.group.title).toEqual(
19
+ 'Section (Group)',
20
+ );
21
+ expect(newConfig.blocks.blocksConfig.group.icon).toBeDefined();
22
+ expect(newConfig.blocks.blocksConfig.group.view).toBeDefined();
23
+ expect(newConfig.blocks.blocksConfig.group.edit).toBeDefined();
24
+ expect(newConfig.blocks.blocksConfig.group.schema).toBeDefined();
25
+ expect(newConfig.blocks.blocksConfig.group.restricted).toEqual(false);
26
+ });
27
+
28
+ it('should include allowed blocks in schema', () => {
29
+ const config = {
30
+ blocks: {
31
+ blocksConfig: {
32
+ text: { title: 'Text', restricted: false },
33
+ image: { restricted: false },
34
+ image_test: { title: 'Image', restricted: true },
35
+ },
36
+ },
37
+ };
38
+
39
+ const newConfig = applyConfig(config);
40
+
41
+ expect(
42
+ newConfig.blocks.blocksConfig.group.schema.properties.allowedBlocks.items
43
+ .choices,
44
+ ).toEqual([
45
+ ['text', 'Text'],
46
+ ['image', 'image'],
47
+ ['group', 'Group'],
48
+ ]);
49
+ });
50
+
51
+ it('should generate tocEntries correctly', () => {
52
+ const config = {
53
+ blocks: {
54
+ blocksConfig: {},
55
+ },
56
+ };
57
+
58
+ const block = {
59
+ data: {
60
+ blocks: {
61
+ block1: { value: [{ type: 'h1' }], plaintext: 'Heading 1' },
62
+ block2: { value: [{ type: 'h2' }], plaintext: 'Heading 2' },
63
+ block3: { value: [{ type: 'h3' }], plaintext: 'Heading 3' }, // This should be ignored
64
+ },
65
+ blocks_layout: {
66
+ items: ['block1', 'block2', 'block3'],
67
+ },
68
+ },
69
+ };
70
+ const tocData = {
71
+ levels: ['h1', 'h2'],
72
+ };
73
+ const newConfig = applyConfig(config);
74
+ const entries = newConfig.blocks.blocksConfig.group.tocEntries(
75
+ block,
76
+ tocData,
77
+ );
78
+ expect(entries).toEqual([
79
+ [1, 'Heading 1', 'block1'],
80
+ [2, 'Heading 2', 'block2'],
81
+ ]);
82
+ });
83
+
84
+ it('should generate no entries', () => {
85
+ const config = {
86
+ blocks: {
87
+ blocksConfig: {},
88
+ },
89
+ };
90
+ const block = undefined;
91
+ const tocData = {
92
+ levels: undefined,
93
+ };
94
+ const newConfig = applyConfig(config);
95
+ const entries = newConfig.blocks.blocksConfig.group.tocEntries(
96
+ block,
97
+ tocData,
98
+ );
99
+ expect(entries).toEqual([]);
100
+ });
101
+ });
@@ -1 +0,0 @@
1
- module.exports = require('@plone/volto/babel');