@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 +29 -0
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/src/components/manage/Blocks/Search/components/SelectStyling.jsx +1 -2
- package/src/components/manage/Edit/Edit.jsx +11 -2
- package/src/components/manage/Widgets/DatetimeWidget.jsx +2 -0
- package/src/components/theme/Footer/Footer.jsx +1 -0
- package/src/components/theme/Navigation/Navigation.jsx +1 -1
- package/src/components/theme/SkipLinks/SkipLinks.jsx +3 -1
- package/src/components/theme/SkipLinks/SkipLinks.test.jsx +6 -3
- package/src/helpers/Blocks/Blocks.js +120 -48
- package/src/helpers/Blocks/Blocks.test.js +75 -0
- package/src/helpers/Html/Html.jsx +1 -1
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
|
@@ -35,7 +35,11 @@ import {
|
|
|
35
35
|
getSchema,
|
|
36
36
|
listActions,
|
|
37
37
|
} from '@plone/volto/actions';
|
|
38
|
-
import {
|
|
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(
|
|
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"
|
|
@@ -50,7 +50,7 @@ const Navigation = (props) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<nav className="navigation" id="navigation" aria-label="
|
|
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="
|
|
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
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
[idTrailingBlock]: {
|
|
170
|
-
'@type': settings.defaultBlockType,
|
|
171
|
-
},
|
|
172
|
-
}),
|
|
183
|
+
selected: id,
|
|
173
184
|
},
|
|
174
|
-
|
|
175
|
-
},
|
|
185
|
+
}),
|
|
176
186
|
];
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
/**
|
|
180
|
-
*
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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(
|
|
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
|
-
|
|
243
|
-
|
|
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(
|