@plone/volto 17.2.0 → 17.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
@@ -8,6 +8,35 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 17.3.0 (2023-10-27)
12
+
13
+ ### Feature
14
+
15
+ - Updated aria-label for landmarks @ichim-david
16
+ Added landmark on sidebar @ichim-david
17
+ Added Pluggable section for skiplinks @ichim-david [#5290](https://github.com/plone/volto/issues/5290)
18
+
19
+ ### Bugfix
20
+
21
+ - (FIX): put padding so the text is not clipped #5305 @dobri1408 [#5305](https://github.com/plone/volto/issues/5305)
22
+ - Fix compare translations view @sneridagh [#5327](https://github.com/plone/volto/issues/5327)
23
+ - Fix DatetimeWidget on FF, the button default if no type is set is sending the form. @sneridagh
24
+ See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#formmethod [#5343](https://github.com/plone/volto/issues/5343)
25
+
26
+ ### Internal
27
+
28
+ - For blocks that define their `blockSchema`, call `applyBlockDefaults` when creating the initial data for the blocks form.
29
+ It is now possible to define a block configuration function, `initialValue` that returns the initial value for a block. This is useful in use cases such as container blocks that want to create a complex initial data structure, to avoid the need to call `React.useEffect` on their initial block rendering and thus, avoid complex async "concurent" state mutations.
30
+ The `addBlock`, `mutateBlock`, `insertBlock` now allow passing a `blocksConfig` configuration object
31
+
32
+ @tiberiuichim [#5320](https://github.com/plone/volto/issues/5320)
33
+ - Add a new set of acceptance tests with the multilingual fixture using seamless mode. @sneridagh [#5332](https://github.com/plone/volto/issues/5332)
34
+
35
+ ### Documentation
36
+
37
+ - Fix reference link to installation. @stevepiercy [#5328](https://github.com/plone/volto/issues/5328)
38
+ - Add upgrade docs for users of `@kitconcept/volto-blocks-grid` addon @sneridagh [#5333](https://github.com/plone/volto/issues/5333)
39
+
11
40
  ## 17.2.0 (2023-10-16)
12
41
 
13
42
  ### Feature
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "17.2.0",
12
+ "version": "17.3.0",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "17.2.0",
3
+ "version": "17.3.0",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -31,8 +31,7 @@ export const customSelectStyles = {
31
31
  }),
32
32
  valueContainer: (styles) => ({
33
33
  ...styles,
34
- padding: '0px',
35
- paddingLeft: 0,
34
+ padding: '8px',
36
35
  }),
37
36
  dropdownIndicator: (styles) => ({
38
37
  paddingRight: 0,
@@ -35,7 +35,11 @@ import {
35
35
  getSchema,
36
36
  listActions,
37
37
  } from '@plone/volto/actions';
38
- import { getBaseUrl, hasBlocksData } from '@plone/volto/helpers';
38
+ import {
39
+ flattenToAppURL,
40
+ getBaseUrl,
41
+ hasBlocksData,
42
+ } from '@plone/volto/helpers';
39
43
  import { preloadLazyLibs } from '@plone/volto/helpers/Loadable';
40
44
 
41
45
  import saveSVG from '@plone/volto/icons/save.svg';
@@ -260,7 +264,12 @@ class Edit extends Component {
260
264
 
261
265
  setComparingLanguage(lang, content_id) {
262
266
  this.setState({ comparingLanguage: lang });
263
- this.props.getContent(content_id, null, 'compare_to', null);
267
+ this.props.getContent(
268
+ flattenToAppURL(content_id),
269
+ null,
270
+ 'compare_to',
271
+ null,
272
+ );
264
273
  }
265
274
 
266
275
  form = React.createRef();
@@ -240,6 +240,8 @@ export class DatetimeWidgetComponent extends Component {
240
240
  )}
241
241
  {resettable && (
242
242
  <button
243
+ // FF needs that the type is "button" in order to not POST the form
244
+ type="button"
243
245
  disabled={this.props.isDisabled || !datetime}
244
246
  onClick={() => this.onResetDates()}
245
247
  className="item ui noborder button"
@@ -41,6 +41,7 @@ const Footer = ({ intl }) => {
41
41
  color="grey"
42
42
  textAlign="center"
43
43
  id="footer"
44
+ aria-label="Footer"
44
45
  >
45
46
  <Container>
46
47
  <Segment basic inverted color="grey" className="discreet">
@@ -50,7 +50,7 @@ const Navigation = (props) => {
50
50
  };
51
51
 
52
52
  return (
53
- <nav className="navigation" id="navigation" aria-label="navigation">
53
+ <nav className="navigation" id="navigation" aria-label="Site">
54
54
  <div className="hamburger-wrapper mobile tablet only">
55
55
  <button
56
56
  className={cx('hamburger hamburger--spin', {
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { useIntl, defineMessages } from 'react-intl';
3
+ import { Pluggable } from '@plone/volto/components/manage/Pluggable';
3
4
 
4
5
  const messages = defineMessages({
5
6
  mainView: {
@@ -23,7 +24,7 @@ const SkipLinks = () => {
23
24
  <div
24
25
  className="skiplinks-wrapper"
25
26
  role="complementary"
26
- aria-label="skiplinks"
27
+ aria-label="Skiplinks"
27
28
  >
28
29
  <a className="skiplink" href="#view">
29
30
  {intl.formatMessage(messages.mainView)}
@@ -34,6 +35,7 @@ const SkipLinks = () => {
34
35
  <a className="skiplink" href="#footer">
35
36
  {intl.formatMessage(messages.footer)}
36
37
  </a>
38
+ <Pluggable name="main.skiplinks" />
37
39
  </div>
38
40
  );
39
41
  };
@@ -3,6 +3,7 @@ import renderer from 'react-test-renderer';
3
3
  import configureStore from 'redux-mock-store';
4
4
  import { Provider } from 'react-intl-redux';
5
5
  import { MemoryRouter } from 'react-router-dom';
6
+ import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable';
6
7
 
7
8
  import SkipLinks from './SkipLinks';
8
9
 
@@ -18,9 +19,11 @@ describe('SkipLinks', () => {
18
19
  });
19
20
  const component = renderer.create(
20
21
  <Provider store={store}>
21
- <MemoryRouter>
22
- <SkipLinks />
23
- </MemoryRouter>
22
+ <PluggablesProvider>
23
+ <MemoryRouter>
24
+ <SkipLinks />
25
+ </MemoryRouter>
26
+ </PluggablesProvider>
24
27
  </Provider>,
25
28
  );
26
29
  const json = component.toJSON();
@@ -132,14 +132,14 @@ export function deleteBlock(formData, blockId) {
132
132
  }
133
133
 
134
134
  /**
135
- * Add block
135
+ * Adds a block to the blocks form
136
136
  * @function addBlock
137
137
  * @param {Object} formData Form data
138
138
  * @param {string} type Block type
139
139
  * @param {number} index Destination index
140
140
  * @return {Array} New block id, New form data
141
141
  */
142
- export function addBlock(formData, type, index) {
142
+ export function addBlock(formData, type, index, blocksConfig) {
143
143
  const { settings } = config;
144
144
  const id = uuid();
145
145
  const idTrailingBlock = uuid();
@@ -148,81 +148,133 @@ export function addBlock(formData, type, index) {
148
148
  const totalItems = formData[blocksLayoutFieldname].items.length;
149
149
  const insert = index === -1 ? totalItems : index;
150
150
 
151
+ let value = applyBlockDefaults({
152
+ data: {
153
+ '@type': type,
154
+ },
155
+ intl: _dummyIntl,
156
+ });
157
+
151
158
  return [
152
159
  id,
153
- {
154
- ...formData,
155
- [blocksLayoutFieldname]: {
156
- items: [
157
- ...formData[blocksLayoutFieldname].items.slice(0, insert),
158
- id,
159
- ...(type !== settings.defaultBlockType ? [idTrailingBlock] : []),
160
- ...formData[blocksLayoutFieldname].items.slice(insert),
161
- ],
162
- },
163
- [blocksFieldname]: {
164
- ...formData[blocksFieldname],
165
- [id]: {
166
- '@type': type,
160
+ _applyBlockInitialValue({
161
+ id,
162
+ value,
163
+ blocksConfig,
164
+ formData: {
165
+ ...formData,
166
+ [blocksLayoutFieldname]: {
167
+ items: [
168
+ ...formData[blocksLayoutFieldname].items.slice(0, insert),
169
+ id,
170
+ ...(type !== settings.defaultBlockType ? [idTrailingBlock] : []),
171
+ ...formData[blocksLayoutFieldname].items.slice(insert),
172
+ ],
173
+ },
174
+ [blocksFieldname]: {
175
+ ...formData[blocksFieldname],
176
+ [id]: value,
177
+ ...(type !== settings.defaultBlockType && {
178
+ [idTrailingBlock]: {
179
+ '@type': settings.defaultBlockType,
180
+ },
181
+ }),
167
182
  },
168
- ...(type !== settings.defaultBlockType && {
169
- [idTrailingBlock]: {
170
- '@type': settings.defaultBlockType,
171
- },
172
- }),
183
+ selected: id,
173
184
  },
174
- selected: id,
175
- },
185
+ }),
176
186
  ];
177
187
  }
178
188
 
179
189
  /**
180
- * Mutate block
190
+ * Gets an initial value for a block, based on configuration
191
+ *
192
+ * This allows blocks that need complex initial data structures to avoid having
193
+ * to call `onChangeBlock` at their creation time, as this is prone to racing
194
+ * issue on block data storage.
195
+ */
196
+ const _applyBlockInitialValue = ({ id, value, blocksConfig, formData }) => {
197
+ const blocksFieldname = getBlocksFieldname(formData);
198
+ const type = value['@type'];
199
+ blocksConfig = blocksConfig || config.blocks.blocksConfig;
200
+
201
+ if (blocksConfig[type]?.initialValue) {
202
+ value = blocksConfig[type].initialValue({
203
+ id,
204
+ value,
205
+ formData,
206
+ });
207
+ formData[blocksFieldname][id] = value;
208
+ }
209
+
210
+ return formData;
211
+ };
212
+
213
+ /**
214
+ * Mutate block, changes the block @type
181
215
  * @function mutateBlock
182
216
  * @param {Object} formData Form data
183
217
  * @param {string} id Block uid to mutate
184
218
  * @param {number} value Block's new value
185
219
  * @return {Object} New form data
186
220
  */
187
- export function mutateBlock(formData, id, value) {
221
+ export function mutateBlock(formData, id, value, blocksConfig) {
188
222
  const { settings } = config;
189
223
  const blocksFieldname = getBlocksFieldname(formData);
190
224
  const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
191
225
  const index = formData[blocksLayoutFieldname].items.indexOf(id) + 1;
192
226
 
227
+ value = applyBlockDefaults({
228
+ data: value,
229
+ intl: _dummyIntl,
230
+ });
231
+ let newFormData;
232
+
193
233
  // Test if block at index is already a placeholder (trailing) block
194
234
  const trailId = formData[blocksLayoutFieldname].items[index];
195
235
  if (trailId) {
196
236
  const block = formData[blocksFieldname][trailId];
197
- if (!blockHasValue(block)) {
198
- return {
237
+ newFormData = _applyBlockInitialValue({
238
+ id,
239
+ value,
240
+ blocksConfig,
241
+ formData: {
199
242
  ...formData,
200
243
  [blocksFieldname]: {
201
244
  ...formData[blocksFieldname],
202
245
  [id]: value || null,
203
246
  },
204
- };
247
+ },
248
+ });
249
+ if (!blockHasValue(block)) {
250
+ return newFormData;
205
251
  }
206
252
  }
207
253
 
208
254
  const idTrailingBlock = uuid();
209
- return {
210
- ...formData,
211
- [blocksFieldname]: {
212
- ...formData[blocksFieldname],
213
- [id]: value || null,
214
- [idTrailingBlock]: {
215
- '@type': settings.defaultBlockType,
255
+ newFormData = _applyBlockInitialValue({
256
+ id,
257
+ value,
258
+ blocksConfig,
259
+ formData: {
260
+ ...formData,
261
+ [blocksFieldname]: {
262
+ ...formData[blocksFieldname],
263
+ [id]: value || null,
264
+ [idTrailingBlock]: {
265
+ '@type': settings.defaultBlockType,
266
+ },
267
+ },
268
+ [blocksLayoutFieldname]: {
269
+ items: [
270
+ ...formData[blocksLayoutFieldname].items.slice(0, index),
271
+ idTrailingBlock,
272
+ ...formData[blocksLayoutFieldname].items.slice(index),
273
+ ],
216
274
  },
217
275
  },
218
- [blocksLayoutFieldname]: {
219
- items: [
220
- ...formData[blocksLayoutFieldname].items.slice(0, index),
221
- idTrailingBlock,
222
- ...formData[blocksLayoutFieldname].items.slice(index),
223
- ],
224
- },
225
- };
276
+ });
277
+ return newFormData;
226
278
  }
227
279
 
228
280
  /**
@@ -233,15 +285,29 @@ export function mutateBlock(formData, id, value) {
233
285
  * @param {number} value New block's value
234
286
  * @return {Array} New block id, New form data
235
287
  */
236
- export function insertBlock(formData, id, value, current = {}, offset = 0) {
288
+ export function insertBlock(
289
+ formData,
290
+ id,
291
+ value,
292
+ current = {},
293
+ offset = 0,
294
+ blocksConfig,
295
+ ) {
237
296
  const blocksFieldname = getBlocksFieldname(formData);
238
297
  const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
239
298
  const index = formData[blocksLayoutFieldname].items.indexOf(id);
240
299
 
300
+ value = applyBlockDefaults({
301
+ data: value,
302
+ intl: _dummyIntl,
303
+ });
304
+
241
305
  const newBlockId = uuid();
242
- return [
243
- newBlockId,
244
- {
306
+ const newFormData = _applyBlockInitialValue({
307
+ id,
308
+ value,
309
+ blocksConfig,
310
+ formData: {
245
311
  ...formData,
246
312
  [blocksFieldname]: {
247
313
  ...formData[blocksFieldname],
@@ -259,7 +325,9 @@ export function insertBlock(formData, id, value, current = {}, offset = 0) {
259
325
  ],
260
326
  },
261
327
  },
262
- ];
328
+ });
329
+
330
+ return [newBlockId, newFormData];
263
331
  }
264
332
 
265
333
  /**
@@ -570,3 +638,7 @@ export function findBlocks(blocks, types, result = []) {
570
638
 
571
639
  return result;
572
640
  }
641
+
642
+ const _dummyIntl = {
643
+ formatMessage() {},
644
+ };
@@ -64,6 +64,24 @@ config.blocks.blocksConfig.text = {
64
64
  }),
65
65
  };
66
66
 
67
+ config.blocks.blocksConfig.dummyText = {
68
+ id: 'dummyText',
69
+ title: 'Text',
70
+ group: 'text',
71
+ restricted: false,
72
+ mostUsed: false,
73
+ blockHasOwnFocusManagement: true,
74
+ blockHasValue: (data) => {
75
+ const isEmpty =
76
+ !data.text ||
77
+ (data.text?.blocks?.length === 1 && data.text.blocks[0].text === '');
78
+ return !isEmpty;
79
+ },
80
+ initialValue: ({ value, id, formData }) => {
81
+ return { ...value, marker: true };
82
+ },
83
+ };
84
+
67
85
  config.blocks.blocksConfig.enhancedBlock = {
68
86
  id: 'enhancedBlock',
69
87
  title: 'Text',
@@ -474,6 +492,63 @@ describe('Blocks', () => {
474
492
  );
475
493
  expect(form.blocks_layout.items).toStrictEqual(['a', newId, 'b']);
476
494
  });
495
+
496
+ it('initializes data for new block with initialValue', () => {
497
+ const [newId, form] = addBlock(
498
+ {
499
+ blocks: { a: { value: 1 }, b: { value: 2 } },
500
+ blocks_layout: { items: ['a', 'b'] },
501
+ },
502
+ 'dummyText',
503
+ 1,
504
+ );
505
+ expect(form.blocks[newId]).toStrictEqual({
506
+ '@type': 'dummyText',
507
+ marker: true,
508
+ });
509
+ });
510
+
511
+ it('initializes data for new block based on schema defaults', () => {
512
+ const [newId, form] = addBlock(
513
+ {
514
+ blocks: { a: { value: 1 }, b: { value: 2 } },
515
+ blocks_layout: { items: ['a', 'b'] },
516
+ },
517
+ 'text',
518
+ 1,
519
+ );
520
+ expect(form.blocks[newId]).toStrictEqual({
521
+ '@type': 'text',
522
+ booleanField: false,
523
+ description: 'Default description',
524
+ title: 'Default title',
525
+ });
526
+ });
527
+
528
+ it('initializes data for new block based on schema defaults and initialValue', () => {
529
+ config.blocks.blocksConfig.text.initialValue = ({ value }) => ({
530
+ ...value,
531
+ marker: true,
532
+ });
533
+ const [newId, form] = addBlock(
534
+ {
535
+ blocks: { a: { value: 1 }, b: { value: 2 } },
536
+ blocks_layout: { items: ['a', 'b'] },
537
+ },
538
+ 'text',
539
+ 1,
540
+ );
541
+
542
+ delete config.blocks.blocksConfig.text.initialValue;
543
+
544
+ expect(form.blocks[newId]).toStrictEqual({
545
+ '@type': 'text',
546
+ booleanField: false,
547
+ description: 'Default description',
548
+ title: 'Default title',
549
+ marker: true,
550
+ });
551
+ });
477
552
  });
478
553
 
479
554
  describe('moveBlock', () => {
@@ -177,7 +177,7 @@ class Html extends Component {
177
177
  <body className={bodyClass}>
178
178
  <div role="navigation" aria-label="Toolbar" id="toolbar" />
179
179
  <div id="main" dangerouslySetInnerHTML={{ __html: markup }} />
180
- <div id="sidebar" />
180
+ <div role="complementary" aria-label="Sidebar" id="sidebar" />
181
181
  <script
182
182
  dangerouslySetInnerHTML={{
183
183
  __html: `window.__data=${serialize(