@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 +30 -0
- package/DEVELOP.md +56 -0
- package/README.md +4 -0
- package/docker-compose.yml +32 -0
- package/locales/de/LC_MESSAGES/volto.po +22 -0
- package/locales/en/LC_MESSAGES/volto.po +22 -0
- package/locales/it/LC_MESSAGES/volto.po +22 -0
- package/locales/ro/LC_MESSAGES/volto.po +22 -0
- package/locales/volto.pot +24 -0
- package/package.json +1 -1
- package/src/components/index.js +4 -3
- package/src/components/manage/Blocks/Group/Body.jsx +17 -0
- package/src/components/manage/Blocks/Group/CounterComponent.jsx +39 -32
- package/src/components/manage/Blocks/Group/CounterComponent.test.jsx +234 -0
- package/src/components/manage/Blocks/Group/DefaultBody.jsx +105 -0
- package/src/components/manage/Blocks/Group/Edit.jsx +27 -96
- package/src/components/manage/Blocks/Group/Edit.test.jsx +126 -1
- package/src/components/manage/Blocks/Group/View.jsx +4 -4
- package/src/components/manage/Blocks/Group/View.test.jsx +12 -1
- package/src/index.js +14 -2
- package/src/index.test.js +101 -0
- package/.i18n.babel.config.js +0 -1
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
package/src/components/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
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
|
|
12
|
-
const
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
16
|
+
const countCharsWithSpaces = (paragraph) => {
|
|
17
|
+
return paragraph?.length || 0;
|
|
18
|
+
};
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return groupCharCount;
|
|
30
|
-
}
|
|
31
|
-
if (!blocksObject) return groupCharCount;
|
|
33
|
+
groupCharCount.value += ignoreSpaces
|
|
34
|
+
? countCharsWithoutSpaces(foundText)
|
|
35
|
+
: countCharsWithSpaces(foundText);
|
|
36
|
+
};
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
43
|
+
if (!maxChars || !blocksObject) {
|
|
44
|
+
return groupCharCount.value;
|
|
45
|
+
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
visitBlocks(
|
|
48
|
+
blocksObject,
|
|
49
|
+
countTextInEachBlock(countTextIn, ignoreSpaces, groupCharCount),
|
|
50
|
+
);
|
|
48
51
|
|
|
49
|
-
|
|
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 {
|
|
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
|
|
27
|
+
const childBlocksForm = isEmpty(data_blocks) ? emptyBlocksForm() : data.data;
|
|
38
28
|
|
|
39
29
|
const [selectedBlock, setSelectedBlock] = useState(
|
|
40
|
-
|
|
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
|
-
|
|
123
|
+
childBlocksForm.blocks_layout.items[0] !== selectedBlock
|
|
135
124
|
) {
|
|
136
|
-
setSelectedBlock(
|
|
125
|
+
setSelectedBlock(childBlocksForm.blocks_layout.items[0]);
|
|
137
126
|
onChangeBlock(block, {
|
|
138
127
|
...data,
|
|
139
|
-
data:
|
|
128
|
+
data: childBlocksForm,
|
|
140
129
|
});
|
|
141
130
|
}
|
|
142
|
-
}, [onChangeBlock,
|
|
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 {
|
|
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
|
-
<
|
|
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 {
|
|
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
|
+
});
|
package/.i18n.babel.config.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('@plone/volto/babel');
|