@eeacms/volto-group-block 6.1.2 → 6.2.1

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,28 @@ 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.2.1](https://github.com/eea/volto-group-block/compare/6.2.0...6.2.1) - 18 August 2023
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix: ignoreSpaces on char counter - refs #256525 [rexalex - [`8d1ad2c`](https://github.com/eea/volto-group-block/commit/8d1ad2cfdf972b88f02f9efc4b7ef55b1e4b3592)]
12
+
13
+ #### :house: Documentation changes
14
+
15
+ - docs: Cleanup Makefile, update DEVELOP documentation, i18n - refs #254894 [valentinab25 - [`94b58f7`](https://github.com/eea/volto-group-block/commit/94b58f7c0390bfcac24adbfbea61577137c91e2b)]
16
+
17
+ #### :hammer_and_wrench: Others
18
+
19
+ - test: increase test coverage - refs #254313 [ana-oprea - [`5facade`](https://github.com/eea/volto-group-block/commit/5facadec2f866a066ca836f8247a5e58ea44d176)]
20
+ ### [6.2.0](https://github.com/eea/volto-group-block/compare/6.1.2...6.2.0) - 20 July 2023
21
+
22
+ #### :nail_care: Enhancements
23
+
24
+ - refactor: char-count component refs #253801 [Nilesh - [`1a54719`](https://github.com/eea/volto-group-block/commit/1a54719af523663314190741417bb06142967f68)]
25
+
26
+ #### :hammer_and_wrench: Others
27
+
28
+ - Release 6.2.0 [Alin Voinea - [`e4e254d`](https://github.com/eea/volto-group-block/commit/e4e254da4c109ee1c8c8a9ce42821eeabfae06ef)]
7
29
  ### [6.1.2](https://github.com/eea/volto-group-block/compare/6.1.1...6.1.2) - 12 June 2023
8
30
 
9
31
  #### :house: Internal changes
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,28 @@
1
+ version: "3"
2
+ services:
3
+ backend:
4
+ image: plone/plone-backend:${PLONE_VERSION:-6}
5
+ ports:
6
+ - "8080:8080"
7
+ environment:
8
+ SITE: "Plone"
9
+
10
+ frontend:
11
+ build:
12
+ context: ./
13
+ dockerfile: ./Dockerfile
14
+ args:
15
+ ADDON_NAME: "${ADDON_NAME}"
16
+ ADDON_PATH: "${ADDON_PATH}"
17
+ VOLTO_VERSION: ${VOLTO_VERSION:-16}
18
+ ports:
19
+ - "3000:3000"
20
+ - "3001:3001"
21
+ depends_on:
22
+ - backend
23
+ volumes:
24
+ - ./:/app/src/addons/${ADDON_PATH}
25
+ environment:
26
+ RAZZLE_INTERNAL_API_PATH: "http://backend:8080/Plone"
27
+ RAZZLE_DEV_PROXY_API_PATH: "http://backend:8080/Plone"
28
+ HOST: "0.0.0.0"
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,14 @@
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
+
package/locales/volto.pot CHANGED
@@ -0,0 +1,16 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "Project-Id-Version: Plone\n"
4
+ "POT-Creation-Date: 2023-06-28T10:48:22.678Z\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
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-group-block",
3
- "version": "6.1.2",
3
+ "version": "6.2.1",
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",
@@ -0,0 +1,91 @@
1
+ import cx from 'classnames';
2
+ import isString from 'lodash/isString';
3
+ import isArray from 'lodash/isArray';
4
+ import { Icon } from '@plone/volto/components';
5
+ import config from '@plone/volto/registry';
6
+ import { visitBlocks } from '@plone/volto/helpers/Blocks/Blocks';
7
+ import { serializeNodesToText } from '@plone/volto-slate/editor/render';
8
+ import delightedSVG from '@plone/volto/icons/delighted.svg';
9
+ import dissatisfiedSVG from '@plone/volto/icons/dissatisfied.svg';
10
+
11
+ const countCharsWithoutSpaces = (paragraph) => {
12
+ const regex = /[^\s\\]/g;
13
+ return (paragraph.match(regex) || []).length;
14
+ };
15
+
16
+ const countCharsWithSpaces = (paragraph) => {
17
+ return paragraph?.length || 0;
18
+ };
19
+
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
+ : '';
32
+
33
+ groupCharCount.value += ignoreSpaces
34
+ ? countCharsWithoutSpaces(foundText)
35
+ : countCharsWithSpaces(foundText);
36
+ };
37
+
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 };
42
+
43
+ if (!maxChars || !blocksObject) {
44
+ return groupCharCount.value;
45
+ }
46
+
47
+ visitBlocks(
48
+ blocksObject,
49
+ countTextInEachBlock(countTextIn, ignoreSpaces, groupCharCount),
50
+ );
51
+
52
+ return groupCharCount.value;
53
+ };
54
+
55
+ const CounterComponent = ({ data, setSidebarTab, setSelectedBlock }) => {
56
+ const { maxChars, ignoreSpaces } = data;
57
+ const charCount = countTextInBlocks(data?.data, ignoreSpaces, maxChars);
58
+ const counterClass =
59
+ charCount < Math.ceil(maxChars / 1.05)
60
+ ? 'info'
61
+ : charCount < maxChars
62
+ ? 'warning'
63
+ : 'danger';
64
+
65
+ return (
66
+ <p
67
+ className={cx('counter', counterClass)}
68
+ onClick={() => {
69
+ setSelectedBlock();
70
+ setSidebarTab(1);
71
+ }}
72
+ aria-hidden="true"
73
+ >
74
+ {maxChars - charCount < 0 ? (
75
+ <>
76
+ <span>{`${charCount - maxChars} characters over the limit`}</span>
77
+ <Icon name={dissatisfiedSVG} size="24px" />
78
+ </>
79
+ ) : (
80
+ <>
81
+ <span>{`${
82
+ maxChars - charCount
83
+ } characters remaining out of ${maxChars}`}</span>
84
+ <Icon name={delightedSVG} size="24px" />
85
+ </>
86
+ )}
87
+ </p>
88
+ );
89
+ };
90
+
91
+ export default CounterComponent;
@@ -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
+ });
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useCallback } from 'react';
2
2
  import { isEmpty, without } from 'lodash';
3
3
  import config from '@plone/volto/registry';
4
4
  import {
@@ -12,14 +12,12 @@ import {
12
12
  emptyBlocksForm,
13
13
  getBlocksLayoutFieldname,
14
14
  } from '@plone/volto/helpers';
15
- import delightedSVG from '@plone/volto/icons/delighted.svg';
16
- import dissatisfiedSVG from '@plone/volto/icons/dissatisfied.svg';
17
15
  import PropTypes from 'prop-types';
18
16
  import { Button, Segment } from 'semantic-ui-react';
19
17
  import EditBlockWrapper from './EditBlockWrapper';
20
18
  import EditSchema from './EditSchema';
19
+ import CounterComponent from './CounterComponent';
21
20
  import helpSVG from '@plone/volto/icons/help.svg';
22
- import cx from 'classnames';
23
21
  import './editor.less';
24
22
 
25
23
  const Edit = (props) => {
@@ -43,8 +41,6 @@ const Edit = (props) => {
43
41
  );
44
42
 
45
43
  const blockState = {};
46
- let charCount = 0;
47
-
48
44
  const handleKeyDown = (
49
45
  e,
50
46
  index,
@@ -71,47 +67,48 @@ const Edit = (props) => {
71
67
  }
72
68
  };
73
69
 
74
- const onSelectBlock = (id, isMultipleSelection, event, activeBlock) => {
75
- let newMultiSelected = [];
76
- let selected = id;
70
+ const onSelectBlock = useCallback(
71
+ (id, isMultipleSelection, event, activeBlock) => {
72
+ let newMultiSelected = [];
73
+ let selected = id;
77
74
 
78
- if (isMultipleSelection) {
79
- selected = null;
80
- const blocksLayoutFieldname = getBlocksLayoutFieldname(data?.data);
81
- const blocks_layout = data?.data[blocksLayoutFieldname].items;
82
- if (event.shiftKey) {
83
- const anchor =
84
- multiSelected.length > 0
85
- ? blocks_layout.indexOf(multiSelected[0])
86
- : blocks_layout.indexOf(activeBlock);
87
- const focus = blocks_layout.indexOf(id);
88
- if (anchor === focus) {
89
- newMultiSelected = [id];
90
- } else if (focus > anchor) {
91
- newMultiSelected = [...blocks_layout.slice(anchor, focus + 1)];
92
- } else {
93
- newMultiSelected = [...blocks_layout.slice(focus, anchor + 1)];
75
+ if (isMultipleSelection) {
76
+ selected = null;
77
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(data?.data);
78
+ const blocks_layout = data?.data[blocksLayoutFieldname].items;
79
+ if (event.shiftKey) {
80
+ const anchor =
81
+ multiSelected.length > 0
82
+ ? blocks_layout.indexOf(multiSelected[0])
83
+ : blocks_layout.indexOf(activeBlock);
84
+ const focus = blocks_layout.indexOf(id);
85
+ if (anchor === focus) {
86
+ newMultiSelected = [id];
87
+ } else if (focus > anchor) {
88
+ newMultiSelected = [...blocks_layout.slice(anchor, focus + 1)];
89
+ } else {
90
+ newMultiSelected = [...blocks_layout.slice(focus, anchor + 1)];
91
+ }
94
92
  }
95
- }
96
- if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
97
- if (multiSelected.includes(id)) {
98
- selected = null;
99
- newMultiSelected = without(multiSelected, id);
100
- } else {
101
- newMultiSelected = [...(multiSelected || []), id];
93
+ if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
94
+ if (multiSelected.includes(id)) {
95
+ selected = null;
96
+ newMultiSelected = without(multiSelected, id);
97
+ } else {
98
+ newMultiSelected = [...(multiSelected || []), id];
99
+ }
102
100
  }
103
101
  }
104
- }
105
102
 
106
- setSelectedBlock(selected);
107
- setMultiSelected(newMultiSelected);
108
- };
103
+ setSelectedBlock(selected);
104
+ setMultiSelected(newMultiSelected);
105
+ },
106
+ [data.data, multiSelected],
107
+ );
109
108
 
110
109
  const changeBlockData = (newBlockData) => {
111
110
  let pastedBlocks = newBlockData.blocks_layout.items.filter((blockID) => {
112
- if (data?.data?.blocks_layout.items.find((x) => x === blockID))
113
- return false;
114
- return true;
111
+ return !data?.data?.blocks_layout.items.find((x) => x === blockID);
115
112
  });
116
113
  const selectedIndex =
117
114
  data.data.blocks_layout.items.indexOf(selectedBlock) + 1;
@@ -144,101 +141,6 @@ const Edit = (props) => {
144
141
  }
145
142
  }, [onChangeBlock, properties, selectedBlock, block, data, data_blocks]);
146
143
 
147
- /**
148
- * Count the number of characters that are anything except using Regex
149
- * @param {string} paragraph
150
- * @returns
151
- */
152
- const countCharsWithoutSpaces = (paragraph) => {
153
- const regex = /[^\s\\]/g;
154
-
155
- return (paragraph.match(regex) || []).length;
156
- };
157
-
158
- /**
159
- * Count the number of characters
160
- * @param {string} paragraph
161
- * @returns
162
- */
163
- const countCharsWithSpaces = (paragraph) => {
164
- return paragraph?.length || 0;
165
- };
166
-
167
- /**
168
- * Recursively look for any block that contains text or plaintext
169
- * @param {Object} blocksObject
170
- * @returns
171
- */
172
- const countTextInBlocks = (blocksObject) => {
173
- let groupCharCount = 0;
174
- if (!props.data.maxChars) {
175
- return groupCharCount;
176
- }
177
-
178
- Object.keys(blocksObject).forEach((blockId) => {
179
- const foundText = blocksObject[blockId]?.plaintext
180
- ? blocksObject[blockId]?.plaintext
181
- : blocksObject[blockId]?.text?.blocks[0]?.text
182
- ? blocksObject[blockId].text.blocks[0].text
183
- : blocksObject[blockId]?.data?.blocks
184
- ? countTextInBlocks(blocksObject[blockId]?.data?.blocks)
185
- : blocksObject[blockId]?.blocks
186
- ? countTextInBlocks(blocksObject[blockId]?.blocks)
187
- : '';
188
- const resultText =
189
- typeof foundText === 'string' || foundText instanceof String
190
- ? foundText
191
- : '';
192
-
193
- groupCharCount += props.data.ignoreSpaces
194
- ? countCharsWithoutSpaces(resultText)
195
- : countCharsWithSpaces(resultText);
196
- });
197
-
198
- return groupCharCount;
199
- };
200
-
201
- const showCharCounter = () => {
202
- if (data_blocks) {
203
- charCount = countTextInBlocks(data_blocks);
204
- }
205
- };
206
- showCharCounter();
207
-
208
- const counterClass =
209
- charCount < Math.ceil(props.data.maxChars / 1.05)
210
- ? 'info'
211
- : charCount < props.data.maxChars
212
- ? 'warning'
213
- : 'danger';
214
-
215
- const counterComponent = props.data.maxChars ? (
216
- <p
217
- className={cx('counter', counterClass)}
218
- onClick={() => {
219
- setSelectedBlock();
220
- props.setSidebarTab(1);
221
- }}
222
- aria-hidden="true"
223
- >
224
- {props.data.maxChars - charCount < 0 ? (
225
- <>
226
- <span>{`${
227
- charCount - props.data.maxChars
228
- } characters over the limit`}</span>
229
- <Icon name={dissatisfiedSVG} size="24px" />
230
- </>
231
- ) : (
232
- <>
233
- <span>{`${
234
- props.data.maxChars - charCount
235
- } characters remaining out of ${props.data.maxChars}`}</span>
236
- <Icon name={delightedSVG} size="24px" />
237
- </>
238
- )}
239
- </p>
240
- ) : null;
241
-
242
144
  // Get editing instructions from block settings or props
243
145
  let instructions = data?.instructions?.data || data?.instructions;
244
146
  if (!instructions || instructions === '<p><br/></p>') {
@@ -355,7 +257,9 @@ const Edit = (props) => {
355
257
  )}
356
258
  </BlocksForm>
357
259
 
358
- {counterComponent}
260
+ {props.data.maxChars && (
261
+ <CounterComponent {...props} setSelectedBlock={setSelectedBlock} />
262
+ )}
359
263
  <SidebarPortal selected={selected && !selectedBlock}>
360
264
  {instructions && (
361
265
  <Segment attached>
@@ -76,4 +76,111 @@ describe('Edit', () => {
76
76
  );
77
77
  fireEvent.keyDown(getByRole('presentation'), { key: 'ArrowUp', code: 38 });
78
78
  });
79
+
80
+ it('should call ArrowUp keydown', () => {
81
+ const props = {
82
+ block: 'testBlock',
83
+ data: {
84
+ instructions: 'test',
85
+ data: {
86
+ blocks: {
87
+ block1: {
88
+ type: 'test',
89
+ data: {
90
+ value: 'Test',
91
+ },
92
+ },
93
+ },
94
+ blocks_layout: {
95
+ items: [undefined],
96
+ },
97
+ },
98
+ },
99
+ onChangeBlock,
100
+ onChangeField,
101
+ pathname: '/',
102
+ selected: true,
103
+ manage: true,
104
+ };
105
+ const mockOnFocusPreviousBlock = jest.fn();
106
+ const mockOnFocusNextBlock = jest.fn();
107
+ const mockOnAddBlock = jest.fn();
108
+ const mockSidebarTab = jest.fn();
109
+
110
+ const { container } = render(
111
+ <Provider store={store}>
112
+ <Edit
113
+ {...props}
114
+ onFocusPreviousBlock={mockOnFocusPreviousBlock}
115
+ onFocusNextBlock={mockOnFocusNextBlock}
116
+ onAddBlock={mockOnAddBlock}
117
+ blockNode={mockBlockNode}
118
+ setSidebarTab={mockSidebarTab}
119
+ />
120
+ </Provider>,
121
+ );
122
+
123
+ fireEvent.keyDown(container.querySelector('.section-block'), {
124
+ key: 'ArrowUp',
125
+ code: 38,
126
+ });
127
+ fireEvent.keyDown(container.querySelector('.section-block'), {
128
+ key: 'ArrowDown',
129
+ code: 40,
130
+ });
131
+ fireEvent.keyDown(container.querySelector('.section-block'), {
132
+ key: 'Enter',
133
+ code: 13,
134
+ });
135
+
136
+ fireEvent.click(container.querySelector('.blocks-form'), {
137
+ shiftKey: true,
138
+ });
139
+ fireEvent.click(container.querySelector('.section-block legend'));
140
+ });
141
+
142
+ it('should call ArrowUp keydown', () => {
143
+ const props = {
144
+ block: 'testBlock',
145
+ data: {
146
+ instructions: 'test',
147
+ data: {
148
+ blocks: {
149
+ block1: {
150
+ type: 'test',
151
+ data: {
152
+ value: 'Test',
153
+ },
154
+ },
155
+ },
156
+ blocks_layout: {
157
+ items: [undefined],
158
+ },
159
+ },
160
+ },
161
+ onChangeBlock,
162
+ onChangeField,
163
+ pathname: '/',
164
+ selected: true,
165
+ manage: true,
166
+ };
167
+ const mockOnFocusPreviousBlock = jest.fn();
168
+ const mockOnFocusNextBlock = jest.fn();
169
+ const mockOnAddBlock = jest.fn();
170
+ const mockSidebarTab = jest.fn();
171
+ const { container } = render(
172
+ <Provider store={store}>
173
+ <Edit
174
+ {...props}
175
+ onFocusPreviousBlock={mockOnFocusPreviousBlock}
176
+ onFocusNextBlock={mockOnFocusNextBlock}
177
+ onAddBlock={mockOnAddBlock}
178
+ setSidebarTab={mockSidebarTab}
179
+ blockNode={mockBlockNode}
180
+ />
181
+ </Provider>,
182
+ );
183
+
184
+ fireEvent.click(container.querySelector('.section-block legend'));
185
+ });
79
186
  });
package/src/index.js CHANGED
@@ -59,6 +59,7 @@ const applyConfig = (config) => {
59
59
  });
60
60
  return entries;
61
61
  },
62
+ countTextIn: ['slate', 'description'], //id of the block whose text should be counted
62
63
  };
63
64
 
64
65
  return config;
@@ -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');