@eeacms/volto-group-block 8.1.0 → 9.0.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,15 +4,23 @@ 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
- ### [8.1.0](https://github.com/eea/volto-group-block/compare/8.0.0...8.1.0) - 3 December 2025
7
+ ### [9.0.0](https://github.com/eea/volto-group-block/compare/8.1.1...9.0.0) - 10 February 2026
8
8
 
9
9
  #### :rocket: New Features
10
10
 
11
- - feat: Add possibility to skip blocks from character counting - refs #293783 [Teodor Voicu - [`c35d76b`](https://github.com/eea/volto-group-block/commit/c35d76bd03476bbcf8324b247ecf909c093d4117)]
11
+ - feat: Persist charCount to block data and add maxCharsOverflowPercent - refs #294806 [Alin Voinea - [`b5bbd75`](https://github.com/eea/volto-group-block/commit/b5bbd7516cb8cb6bcff8541d711fd9fa74a4d92c)]
12
+
13
+ ### [8.1.1](https://github.com/eea/volto-group-block/compare/8.1.0...8.1.1) - 26 January 2026
12
14
 
13
15
  #### :hammer_and_wrench: Others
14
16
 
15
- - Release 8.1.0 [Alin Voinea - [`d711911`](https://github.com/eea/volto-group-block/commit/d71191102f8ef97375905049c7cbbe71ea9e357c)]
17
+ - Fix blocks chooser layout when inside section block [Teodor - [`978ba35`](https://github.com/eea/volto-group-block/commit/978ba35214fc0b22bca04f22f051fdcd25d9812a)]
18
+ ### [8.1.0](https://github.com/eea/volto-group-block/compare/8.0.0...8.1.0) - 3 December 2025
19
+
20
+ #### :rocket: New Features
21
+
22
+ - feat: Add possibility to skip blocks from character counting - refs #293783 [Teodor Voicu - [`c35d76b`](https://github.com/eea/volto-group-block/commit/c35d76bd03476bbcf8324b247ecf909c093d4117)]
23
+
16
24
  ## [8.0.0](https://github.com/eea/volto-group-block/compare/7.1.1...8.0.0) - 3 December 2025
17
25
 
18
26
  #### :boom: Breaking Change
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-group-block",
3
- "version": "8.1.0",
3
+ "version": "9.0.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",
@@ -56,7 +56,7 @@ const countTextInEachBlock =
56
56
  : countCharsWithSpaces(foundText);
57
57
  };
58
58
 
59
- const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
59
+ export const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
60
60
  const { countTextIn, skipBlocksInGroups = [] } =
61
61
  config.blocks?.blocksConfig?.group || {};
62
62
  // use obj ref to update value - if you send number it will not be updated
@@ -82,12 +82,21 @@ const countTextInBlocks = (blocksObject, ignoreSpaces, maxChars) => {
82
82
  };
83
83
 
84
84
  const CounterComponent = ({ data, setSidebarTab, setSelectedBlock }) => {
85
- const { maxChars, ignoreSpaces } = data;
86
- const charCount = countTextInBlocks(data?.data, ignoreSpaces, maxChars);
85
+ const maxChars = parseInt(data.maxChars) || 0;
86
+ const maxCharsOverflowPercent = parseInt(data.maxCharsOverflowPercent) || 0;
87
+ const { ignoreSpaces } = data;
88
+ const charCount =
89
+ data.charCount ?? countTextInBlocks(data?.data, ignoreSpaces, maxChars);
90
+
91
+ const overflowLimit =
92
+ maxCharsOverflowPercent > 0
93
+ ? Math.ceil((maxChars * (100 + maxCharsOverflowPercent)) / 100)
94
+ : maxChars;
95
+
87
96
  const counterClass =
88
97
  charCount < Math.ceil(maxChars / 1.05)
89
98
  ? 'info'
90
- : charCount < maxChars
99
+ : charCount <= overflowLimit
91
100
  ? 'warning'
92
101
  : 'danger';
93
102
 
@@ -231,4 +231,119 @@ describe('CounterComponent', () => {
231
231
  expect(setSidebarTab).toHaveBeenCalledWith(1);
232
232
  expect(setSelectedBlock).toHaveBeenCalled();
233
233
  });
234
+
235
+ it('should render warning class when chars are within overflow zone (maxCharsOverflowPercent=10)', () => {
236
+ const { container, getByText } = render(
237
+ <CounterComponent
238
+ data={{
239
+ maxChars: 100,
240
+ maxCharsOverflowPercent: 10,
241
+ data: {
242
+ blocks: {
243
+ block1: { '@type': 'text', plaintext: 'a'.repeat(105) },
244
+ },
245
+ blocks_layout: {
246
+ items: ['block1'],
247
+ },
248
+ },
249
+ }}
250
+ setSidebarTab={setSidebarTab}
251
+ setSelectedBlock={setSelectedBlock}
252
+ />,
253
+ );
254
+ expect(getByText('5 characters over the limit')).toBeInTheDocument();
255
+ expect(container.querySelector('.counter.warning')).toBeInTheDocument();
256
+ });
257
+
258
+ it('should render danger class when chars exceed overflow zone (maxCharsOverflowPercent=10)', () => {
259
+ const { container, getByText } = render(
260
+ <CounterComponent
261
+ data={{
262
+ maxChars: 100,
263
+ maxCharsOverflowPercent: 10,
264
+ data: {
265
+ blocks: {
266
+ block1: { '@type': 'text', plaintext: 'a'.repeat(111) },
267
+ },
268
+ blocks_layout: {
269
+ items: ['block1'],
270
+ },
271
+ },
272
+ }}
273
+ setSidebarTab={setSidebarTab}
274
+ setSelectedBlock={setSelectedBlock}
275
+ />,
276
+ );
277
+ expect(getByText('11 characters over the limit')).toBeInTheDocument();
278
+ expect(container.querySelector('.counter.danger')).toBeInTheDocument();
279
+ });
280
+
281
+ it('should render warning class when chars equal maxChars with no overflow percent', () => {
282
+ const { container, getByText } = render(
283
+ <CounterComponent
284
+ data={{
285
+ maxChars: 100,
286
+ data: {
287
+ blocks: {
288
+ block1: { '@type': 'text', plaintext: 'a'.repeat(100) },
289
+ },
290
+ blocks_layout: {
291
+ items: ['block1'],
292
+ },
293
+ },
294
+ }}
295
+ setSidebarTab={setSidebarTab}
296
+ setSelectedBlock={setSelectedBlock}
297
+ />,
298
+ );
299
+ expect(getByText('0 characters remaining out of 100')).toBeInTheDocument();
300
+ expect(container.querySelector('.counter.warning')).toBeInTheDocument();
301
+ });
302
+
303
+ it('should render danger class when negative overflow percent falls back to maxChars', () => {
304
+ const { container, getByText } = render(
305
+ <CounterComponent
306
+ data={{
307
+ maxChars: 100,
308
+ maxCharsOverflowPercent: -5,
309
+ data: {
310
+ blocks: {
311
+ block1: { '@type': 'text', plaintext: 'a'.repeat(104) },
312
+ },
313
+ blocks_layout: {
314
+ items: ['block1'],
315
+ },
316
+ },
317
+ }}
318
+ setSidebarTab={setSidebarTab}
319
+ setSelectedBlock={setSelectedBlock}
320
+ />,
321
+ );
322
+ expect(getByText('4 characters over the limit')).toBeInTheDocument();
323
+ expect(container.querySelector('.counter.danger')).toBeInTheDocument();
324
+ });
325
+
326
+ it('should use persisted charCount from data instead of recomputing', () => {
327
+ const { container, getByText } = render(
328
+ <CounterComponent
329
+ data={{
330
+ maxChars: 100,
331
+ charCount: 42,
332
+ data: {
333
+ blocks: {
334
+ block1: { '@type': 'text', plaintext: 'a'.repeat(90) },
335
+ },
336
+ blocks_layout: {
337
+ items: ['block1'],
338
+ },
339
+ },
340
+ }}
341
+ setSidebarTab={setSidebarTab}
342
+ setSelectedBlock={setSelectedBlock}
343
+ />,
344
+ );
345
+ // Should use charCount=42 from data, not 90 from blocks
346
+ expect(getByText('58 characters remaining out of 100')).toBeInTheDocument();
347
+ expect(container.querySelector('.counter.info')).toBeInTheDocument();
348
+ });
234
349
  });
@@ -1,6 +1,7 @@
1
1
  import { Button } from 'semantic-ui-react';
2
2
  import { BlocksForm, Icon, RenderBlocks } from '@plone/volto/components';
3
3
  import EditBlockWrapper from './EditBlockWrapper';
4
+ import { countTextInBlocks } from './CounterComponent';
4
5
  import { useLocation } from 'react-router-dom';
5
6
 
6
7
  import helpSVG from '@plone/volto/icons/help.svg';
@@ -48,21 +49,38 @@ const GroupBlockDefaultBody = (props) => {
48
49
  onSelectBlock(id, isMultipleSelection, e, selectedBlock);
49
50
  }}
50
51
  onChangeFormData={(newFormData) => {
51
- onChangeBlock(block, {
52
+ const newData = {
52
53
  ...data,
53
54
  data: newFormData,
54
- });
55
+ };
56
+ if (data.maxChars) {
57
+ newData.charCount = countTextInBlocks(
58
+ newFormData,
59
+ data.ignoreSpaces,
60
+ data.maxChars,
61
+ );
62
+ }
63
+ onChangeBlock(block, newData);
55
64
  }}
56
65
  onChangeField={(id, value) => {
57
66
  if (['blocks', 'blocks_layout'].indexOf(id) > -1) {
58
67
  blockState[id] = value;
59
- onChangeBlock(block, {
68
+ const newChildData = {
69
+ ...data.data,
70
+ ...blockState,
71
+ };
72
+ const newData = {
60
73
  ...data,
61
- data: {
62
- ...data.data,
63
- ...blockState,
64
- },
65
- });
74
+ data: newChildData,
75
+ };
76
+ if (data.maxChars) {
77
+ newData.charCount = countTextInBlocks(
78
+ newChildData,
79
+ data.ignoreSpaces,
80
+ data.maxChars,
81
+ );
82
+ }
83
+ onChangeBlock(block, newData);
66
84
  } else {
67
85
  onChangeField(id, value);
68
86
  }
@@ -18,7 +18,7 @@ import PropTypes from 'prop-types';
18
18
  import { Segment } from 'semantic-ui-react';
19
19
  import EditSchema from './EditSchema';
20
20
 
21
- import CounterComponent from './CounterComponent';
21
+ import CounterComponent, { countTextInBlocks } from './CounterComponent';
22
22
  import './editor.less';
23
23
  import { defineMessages, injectIntl } from 'react-intl';
24
24
  import { compose } from 'redux';
@@ -115,20 +115,29 @@ const Edit = (props) => {
115
115
  });
116
116
  const selectedIndex =
117
117
  data.data.blocks_layout.items.indexOf(selectedBlock) + 1;
118
- onChangeBlock(block, {
119
- ...data,
120
- data: {
121
- ...data?.data,
122
- ...newBlockData,
123
- blocks_layout: {
124
- items: [
125
- ...data.data.blocks_layout.items.slice(0, selectedIndex),
126
- ...pastedBlocks,
127
- ...data.data.blocks_layout.items.slice(selectedIndex),
128
- ],
129
- },
118
+ const newChildData = {
119
+ ...data?.data,
120
+ ...newBlockData,
121
+ blocks_layout: {
122
+ items: [
123
+ ...data.data.blocks_layout.items.slice(0, selectedIndex),
124
+ ...pastedBlocks,
125
+ ...data.data.blocks_layout.items.slice(selectedIndex),
126
+ ],
130
127
  },
131
- });
128
+ };
129
+ const newData = {
130
+ ...data,
131
+ data: newChildData,
132
+ };
133
+ if (data.maxChars) {
134
+ newData.charCount = countTextInBlocks(
135
+ newChildData,
136
+ data.ignoreSpaces,
137
+ data.maxChars,
138
+ );
139
+ }
140
+ onChangeBlock(block, newData);
132
141
  };
133
142
 
134
143
  React.useEffect(() => {
@@ -137,10 +146,14 @@ const Edit = (props) => {
137
146
  childBlocksForm.blocks_layout.items[0] !== selectedBlock
138
147
  ) {
139
148
  setSelectedBlock(childBlocksForm.blocks_layout.items[0]);
140
- onChangeBlock(block, {
149
+ const newData = {
141
150
  ...data,
142
151
  data: childBlocksForm,
143
- });
152
+ };
153
+ if (data.maxChars) {
154
+ newData.charCount = 0;
155
+ }
156
+ onChangeBlock(block, newData);
144
157
  }
145
158
  }, [onChangeBlock, childBlocksForm, selectedBlock, block, data, data_blocks]);
146
159
 
@@ -218,10 +231,24 @@ const Edit = (props) => {
218
231
  title={props.intl.formatMessage(messages.sectionGroupSettings)}
219
232
  formData={data}
220
233
  onChangeField={(id, value) => {
221
- props.onChangeBlock(props.block, {
234
+ const newData = {
222
235
  ...props.data,
223
236
  [id]: value,
224
- });
237
+ };
238
+ const effectiveMaxChars =
239
+ id === 'maxChars' ? value : props.data.maxChars;
240
+ const effectiveIgnoreSpaces =
241
+ id === 'ignoreSpaces' ? value : props.data.ignoreSpaces;
242
+ if (effectiveMaxChars) {
243
+ newData.charCount = countTextInBlocks(
244
+ props.data?.data,
245
+ effectiveIgnoreSpaces,
246
+ effectiveMaxChars,
247
+ );
248
+ } else {
249
+ delete newData.charCount;
250
+ }
251
+ props.onChangeBlock(props.block, newData);
225
252
  }}
226
253
  />
227
254
  )}
@@ -11,6 +11,7 @@ const Schema = {
11
11
  'allowedBlocks',
12
12
  'as',
13
13
  'maxChars',
14
+ 'maxCharsOverflowPercent',
14
15
  'ignoreSpaces',
15
16
  'readOnlySettings',
16
17
  'disableInnerButtons',
@@ -68,6 +69,13 @@ const Schema = {
68
69
  type: 'integer',
69
70
  factory: 'Integer',
70
71
  },
72
+ maxCharsOverflowPercent: {
73
+ title: 'Maximum Characters Overflow Percent',
74
+ description:
75
+ 'The percentage of characters that can overflow the maximum characters limit before showing a warning: 0-100',
76
+ type: 'integer',
77
+ factory: 'Integer',
78
+ },
71
79
  ignoreSpaces: {
72
80
  title: 'Ignore spaces',
73
81
  description: 'Ignore spaces while calculating maximum characters',
@@ -71,8 +71,9 @@
71
71
  }
72
72
 
73
73
  .blocks-chooser {
74
- right: 0;
75
- left: auto;
74
+ position: absolute;
75
+ right: 0 !important;
76
+ left: auto !important;
76
77
  margin-top: 3rem;
77
78
  }
78
79