@plone/volto 18.6.0 → 18.7.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/.registry.loader.js +36 -0
- package/CHANGELOG.md +23 -0
- package/cypress/support/commands.js +22 -0
- package/package.json +4 -4
- package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +15 -2
- package/src/components/manage/Widgets/RegistryImageWidget.jsx +15 -16
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +23 -0
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +135 -0
- package/src/components/theme/View/View.jsx +2 -0
- package/src/hooks/clipboard/useClipboard.js +7 -3
- package/types/components/manage/Widgets/ObjectBrowserWidget.d.ts +1 -0
- package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.d.ts +1 -0
- package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.d.ts +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This file is autogenerated. Don't change it directly.
|
|
3
|
+
Instead, change the "addons" setting in your package.json file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const addonsInfo = [];
|
|
7
|
+
export { addonsInfo };
|
|
8
|
+
|
|
9
|
+
const safeWrapper = (func) => (config) => {
|
|
10
|
+
const res = func(config);
|
|
11
|
+
if (typeof res === 'undefined') {
|
|
12
|
+
throw new Error("Configuration function doesn't return config");
|
|
13
|
+
}
|
|
14
|
+
return res;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const projectConfigLoader = false;
|
|
18
|
+
const projectConfig = (config) => {
|
|
19
|
+
return projectConfigLoader &&
|
|
20
|
+
typeof projectConfigLoader.default === 'function'
|
|
21
|
+
? projectConfigLoader.default(config)
|
|
22
|
+
: config;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const load = (config) => {
|
|
26
|
+
const addonLoaders = [];
|
|
27
|
+
if (!addonLoaders.every((el) => typeof el === 'function')) {
|
|
28
|
+
throw new TypeError(
|
|
29
|
+
'Each addon has to provide a function applying its configuration to the projects configuration.',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return projectConfig(
|
|
33
|
+
addonLoaders.reduce((acc, apply) => safeWrapper(apply)(acc), config),
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
export default load;
|
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,29 @@ myst:
|
|
|
17
17
|
|
|
18
18
|
<!-- towncrier release notes start -->
|
|
19
19
|
|
|
20
|
+
## 18.7.0 (2025-01-24)
|
|
21
|
+
|
|
22
|
+
### Feature
|
|
23
|
+
|
|
24
|
+
- - Fixed handling of the site logo preview to appear after upload. @Shyam-Raghuwanshi [#6591](https://github.com/plone/volto/issues/6591)
|
|
25
|
+
- Provide language alternate links @erral [#6602](https://github.com/plone/volto/issues/6602)
|
|
26
|
+
- feat(cypress):Add custom check Accessibility command @Tishasoumya-02 [#6606](https://github.com/plone/volto/issues/6606)
|
|
27
|
+
|
|
28
|
+
### Bugfix
|
|
29
|
+
|
|
30
|
+
- Improve the usability of the `ObjectBrowser` when inputting a manual value, checking it on blur, and adding a local validator. @sneridagh [#6576](https://github.com/plone/volto/issues/6576)
|
|
31
|
+
- fix(useClipboard): Do not have a pending promise in a boolean state @nileshgulia1 [#6585](https://github.com/plone/volto/issues/6585)
|
|
32
|
+
|
|
33
|
+
### Internal
|
|
34
|
+
|
|
35
|
+
- Add Seven convenience Makefile commands. @sneridagh [#6599](https://github.com/plone/volto/issues/6599)
|
|
36
|
+
- Restore pull request previews on Read the Docs. @stevepiercy [#6612](https://github.com/plone/volto/issues/6612)
|
|
37
|
+
- Fix lint-staged throwing warnings when a file is checked-in and ignored. @sneridagh [#6614](https://github.com/plone/volto/issues/6614)
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
- Enhancements of the upgrade guide for Volto 18, since we detected some inconsistencies. @sneridagh [#6609](https://github.com/plone/volto/issues/6609)
|
|
42
|
+
|
|
20
43
|
## 18.6.0 (2025-01-11)
|
|
21
44
|
|
|
22
45
|
### Feature
|
|
@@ -967,3 +967,25 @@ Cypress.Commands.add('queryCounter', (path, steps, number = 1) => {
|
|
|
967
967
|
|
|
968
968
|
cy.get('@counterName').its('callCount').should('equal', number);
|
|
969
969
|
});
|
|
970
|
+
|
|
971
|
+
// Print cypress-axe violations to the terminal
|
|
972
|
+
function printAccessibilityViolations(violations) {
|
|
973
|
+
cy.task(
|
|
974
|
+
'table',
|
|
975
|
+
violations.map(({ id, impact, description, nodes }) => ({
|
|
976
|
+
impact,
|
|
977
|
+
description: `${description} (${id})`,
|
|
978
|
+
nodes: nodes.length,
|
|
979
|
+
})),
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
Cypress.Commands.add(
|
|
984
|
+
'checkAccessibility',
|
|
985
|
+
(subject, { skipFailures = false } = {}) => {
|
|
986
|
+
cy.checkA11y(subject, null, printAccessibilityViolations, skipFailures);
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
prevSubject: 'optional',
|
|
990
|
+
},
|
|
991
|
+
);
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "18.
|
|
12
|
+
"version": "18.7.0",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git@github.com:plone/volto.git"
|
|
@@ -236,8 +236,8 @@
|
|
|
236
236
|
"url": "^0.11.3",
|
|
237
237
|
"use-deep-compare-effect": "1.8.1",
|
|
238
238
|
"uuid": "^8.3.2",
|
|
239
|
-
"@plone/
|
|
240
|
-
"@plone/
|
|
239
|
+
"@plone/volto-slate": "18.2.0",
|
|
240
|
+
"@plone/registry": "2.3.0",
|
|
241
241
|
"@plone/scripts": "3.8.1"
|
|
242
242
|
},
|
|
243
243
|
"devDependencies": {
|
|
@@ -358,7 +358,7 @@
|
|
|
358
358
|
"webpack-dev-server": "4.11.1",
|
|
359
359
|
"webpack-node-externals": "3.0.0",
|
|
360
360
|
"why": "0.6.2",
|
|
361
|
-
"@plone/types": "1.
|
|
361
|
+
"@plone/types": "1.3.0",
|
|
362
362
|
"@plone/volto-coresandbox": "1.0.0"
|
|
363
363
|
},
|
|
364
364
|
"volta": {
|
|
@@ -16,10 +16,10 @@ import { Image, Label, Popup, Button } from 'semantic-ui-react';
|
|
|
16
16
|
import {
|
|
17
17
|
flattenToAppURL,
|
|
18
18
|
isInternalURL,
|
|
19
|
-
isUrl,
|
|
20
19
|
normalizeUrl,
|
|
21
20
|
removeProtocol,
|
|
22
21
|
} from '@plone/volto/helpers/Url/Url';
|
|
22
|
+
import { urlValidator } from '@plone/volto/helpers/FormValidation/validators';
|
|
23
23
|
import { searchContent } from '@plone/volto/actions/search/search';
|
|
24
24
|
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
|
|
25
25
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
@@ -102,6 +102,7 @@ export class ObjectBrowserWidgetComponent extends Component {
|
|
|
102
102
|
state = {
|
|
103
103
|
manualLinkInput: '',
|
|
104
104
|
validURL: false,
|
|
105
|
+
errors: [],
|
|
105
106
|
};
|
|
106
107
|
|
|
107
108
|
constructor(props) {
|
|
@@ -230,7 +231,16 @@ export class ObjectBrowserWidgetComponent extends Component {
|
|
|
230
231
|
|
|
231
232
|
validateManualLink = (url) => {
|
|
232
233
|
if (this.props.allowExternals) {
|
|
233
|
-
|
|
234
|
+
const error = urlValidator({
|
|
235
|
+
value: url,
|
|
236
|
+
formatMessage: this.props.intl.formatMessage,
|
|
237
|
+
});
|
|
238
|
+
if (error && url !== '') {
|
|
239
|
+
this.setState({ errors: [error] });
|
|
240
|
+
} else {
|
|
241
|
+
this.setState({ errors: [] });
|
|
242
|
+
}
|
|
243
|
+
return !Boolean(error);
|
|
234
244
|
} else {
|
|
235
245
|
return isInternalURL(url);
|
|
236
246
|
}
|
|
@@ -344,6 +354,8 @@ export class ObjectBrowserWidgetComponent extends Component {
|
|
|
344
354
|
return (
|
|
345
355
|
<FormFieldWrapper
|
|
346
356
|
{...this.props}
|
|
357
|
+
// At the moment, OBW handles its own errors and validation
|
|
358
|
+
error={this.state.errors}
|
|
347
359
|
className={description ? 'help text' : 'text'}
|
|
348
360
|
>
|
|
349
361
|
<div
|
|
@@ -372,6 +384,7 @@ export class ObjectBrowserWidgetComponent extends Component {
|
|
|
372
384
|
items.length === 0 &&
|
|
373
385
|
this.props.mode !== 'multiple' && (
|
|
374
386
|
<input
|
|
387
|
+
onBlur={this.onSubmitManualLink}
|
|
375
388
|
onKeyDown={this.onKeyDownManualLink}
|
|
376
389
|
onChange={this.onManualLinkInput}
|
|
377
390
|
value={this.state.manualLinkInput}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module components/manage/Widgets/RegistryImageWidget
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from 'react';
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
7
|
import PropTypes from 'prop-types';
|
|
8
8
|
import { Button, Image, Dimmer } from 'semantic-ui-react';
|
|
9
9
|
import { readAsDataURL } from 'promise-file-reader';
|
|
@@ -76,12 +76,15 @@ const RegistryImageWidget = (props) => {
|
|
|
76
76
|
const { id, value, onChange, isDisabled } = props;
|
|
77
77
|
const intl = useIntl();
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)}
|
|
84
|
-
|
|
79
|
+
// State to manage the preview image source
|
|
80
|
+
const [previewSrc, setPreviewSrc] = useState(() => {
|
|
81
|
+
const fileName = value?.split(';')[0];
|
|
82
|
+
return fileName
|
|
83
|
+
? `${toPublicURL('/')}@@site-logo/${atob(
|
|
84
|
+
fileName.replace('filenameb64:', ''),
|
|
85
|
+
)}`
|
|
86
|
+
: '';
|
|
87
|
+
});
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
90
|
* Drop handler
|
|
@@ -102,8 +105,7 @@ const RegistryImageWidget = (props) => {
|
|
|
102
105
|
reader.onload = function () {
|
|
103
106
|
const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
|
|
104
107
|
if (imageMimetypes.includes(fields[1])) {
|
|
105
|
-
|
|
106
|
-
imagePreview.src = reader.result;
|
|
108
|
+
setPreviewSrc(reader.result);
|
|
107
109
|
}
|
|
108
110
|
};
|
|
109
111
|
reader.readAsDataURL(files[0]);
|
|
@@ -115,12 +117,12 @@ const RegistryImageWidget = (props) => {
|
|
|
115
117
|
{({ getRootProps, getInputProps, isDragActive }) => (
|
|
116
118
|
<div className="file-widget-dropzone" {...getRootProps()}>
|
|
117
119
|
{isDragActive && <Dimmer active></Dimmer>}
|
|
118
|
-
{
|
|
120
|
+
{previewSrc ? (
|
|
119
121
|
<Image
|
|
120
122
|
className="image-preview"
|
|
121
123
|
id={`field-${id}-image`}
|
|
122
124
|
size="small"
|
|
123
|
-
src={
|
|
125
|
+
src={previewSrc}
|
|
124
126
|
/>
|
|
125
127
|
) : (
|
|
126
128
|
<div className="dropzone-placeholder">
|
|
@@ -139,7 +141,6 @@ const RegistryImageWidget = (props) => {
|
|
|
139
141
|
)}
|
|
140
142
|
</div>
|
|
141
143
|
)}
|
|
142
|
-
|
|
143
144
|
<label className="label-file-widget-input">
|
|
144
145
|
{value
|
|
145
146
|
? intl.formatMessage(messages.replaceFile)
|
|
@@ -168,6 +169,7 @@ const RegistryImageWidget = (props) => {
|
|
|
168
169
|
disabled={isDisabled}
|
|
169
170
|
onClick={() => {
|
|
170
171
|
onChange(id, '');
|
|
172
|
+
setPreviewSrc(''); // Clear the preview image
|
|
171
173
|
}}
|
|
172
174
|
>
|
|
173
175
|
<Icon name={deleteSVG} size="20px" />
|
|
@@ -189,10 +191,7 @@ RegistryImageWidget.propTypes = {
|
|
|
189
191
|
description: PropTypes.string,
|
|
190
192
|
required: PropTypes.bool,
|
|
191
193
|
error: PropTypes.arrayOf(PropTypes.string),
|
|
192
|
-
value: PropTypes.
|
|
193
|
-
'@type': PropTypes.string,
|
|
194
|
-
title: PropTypes.string,
|
|
195
|
-
}),
|
|
194
|
+
value: PropTypes.string,
|
|
196
195
|
onChange: PropTypes.func.isRequired,
|
|
197
196
|
wrapped: PropTypes.bool,
|
|
198
197
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
3
|
+
|
|
4
|
+
const AlternateHrefLangs = (props) => {
|
|
5
|
+
const { content } = props;
|
|
6
|
+
return (
|
|
7
|
+
<Helmet>
|
|
8
|
+
{config.settings.isMultilingual &&
|
|
9
|
+
content['@components']?.translations?.items?.map((item, key) => {
|
|
10
|
+
return (
|
|
11
|
+
<link
|
|
12
|
+
key={key}
|
|
13
|
+
rel="alternate"
|
|
14
|
+
hrefLang={item.language}
|
|
15
|
+
href={item['@id']}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
})}
|
|
19
|
+
</Helmet>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { AlternateHrefLangs };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
3
|
+
|
|
4
|
+
import renderer from 'react-test-renderer';
|
|
5
|
+
import configureStore from 'redux-mock-store';
|
|
6
|
+
import { Provider } from 'react-intl-redux';
|
|
7
|
+
import config from '@plone/volto/registry';
|
|
8
|
+
|
|
9
|
+
import { AlternateHrefLangs } from './AlternateHrefLangs';
|
|
10
|
+
|
|
11
|
+
const mockStore = configureStore();
|
|
12
|
+
|
|
13
|
+
describe('AlternateHrefLangs', () => {
|
|
14
|
+
beforeEach(() => {});
|
|
15
|
+
it('non multilingual site, renders nothing', () => {
|
|
16
|
+
config.settings.isMultilingual = false;
|
|
17
|
+
const content = {
|
|
18
|
+
'@id': '/',
|
|
19
|
+
'@components': {},
|
|
20
|
+
};
|
|
21
|
+
const store = mockStore({
|
|
22
|
+
intl: {
|
|
23
|
+
locale: 'en',
|
|
24
|
+
messages: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// We need to force the component rendering
|
|
28
|
+
// to fill the Helmet
|
|
29
|
+
renderer.create(
|
|
30
|
+
<Provider store={store}>
|
|
31
|
+
<AlternateHrefLangs content={content} />
|
|
32
|
+
</Provider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
36
|
+
expect(helmetLinks.length).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
it('multilingual site, with some translations', () => {
|
|
39
|
+
config.settings.isMultilingual = true;
|
|
40
|
+
config.settings.supportedLanguages = ['en', 'es', 'eu'];
|
|
41
|
+
|
|
42
|
+
const content = {
|
|
43
|
+
'@components': {
|
|
44
|
+
translations: {
|
|
45
|
+
items: [
|
|
46
|
+
{ '@id': '/en', language: 'en' },
|
|
47
|
+
{ '@id': '/es', language: 'es' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const store = mockStore({
|
|
54
|
+
intl: {
|
|
55
|
+
locale: 'en',
|
|
56
|
+
messages: {},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// We need to force the component rendering
|
|
61
|
+
// to fill the Helmet
|
|
62
|
+
renderer.create(
|
|
63
|
+
<Provider store={store}>
|
|
64
|
+
<>
|
|
65
|
+
<AlternateHrefLangs content={content} />
|
|
66
|
+
</>
|
|
67
|
+
</Provider>,
|
|
68
|
+
);
|
|
69
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
70
|
+
|
|
71
|
+
expect(helmetLinks.length).toBe(2);
|
|
72
|
+
|
|
73
|
+
expect(helmetLinks).toContainEqual({
|
|
74
|
+
rel: 'alternate',
|
|
75
|
+
href: '/es',
|
|
76
|
+
hrefLang: 'es',
|
|
77
|
+
});
|
|
78
|
+
expect(helmetLinks).toContainEqual({
|
|
79
|
+
rel: 'alternate',
|
|
80
|
+
href: '/en',
|
|
81
|
+
hrefLang: 'en',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('multilingual site, with all available translations', () => {
|
|
85
|
+
config.settings.isMultilingual = true;
|
|
86
|
+
config.settings.supportedLanguages = ['en', 'es', 'eu'];
|
|
87
|
+
const store = mockStore({
|
|
88
|
+
intl: {
|
|
89
|
+
locale: 'en',
|
|
90
|
+
messages: {},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const content = {
|
|
95
|
+
'@components': {
|
|
96
|
+
translations: {
|
|
97
|
+
items: [
|
|
98
|
+
{ '@id': '/en', language: 'en' },
|
|
99
|
+
{ '@id': '/eu', language: 'eu' },
|
|
100
|
+
{ '@id': '/es', language: 'es' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// We need to force the component rendering
|
|
107
|
+
// to fill the Helmet
|
|
108
|
+
renderer.create(
|
|
109
|
+
<Provider store={store}>
|
|
110
|
+
<AlternateHrefLangs content={content} />
|
|
111
|
+
</Provider>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
115
|
+
|
|
116
|
+
// We expect having 3 links
|
|
117
|
+
expect(helmetLinks.length).toBe(3);
|
|
118
|
+
|
|
119
|
+
expect(helmetLinks).toContainEqual({
|
|
120
|
+
rel: 'alternate',
|
|
121
|
+
href: '/eu',
|
|
122
|
+
hrefLang: 'eu',
|
|
123
|
+
});
|
|
124
|
+
expect(helmetLinks).toContainEqual({
|
|
125
|
+
rel: 'alternate',
|
|
126
|
+
href: '/es',
|
|
127
|
+
hrefLang: 'es',
|
|
128
|
+
});
|
|
129
|
+
expect(helmetLinks).toContainEqual({
|
|
130
|
+
rel: 'alternate',
|
|
131
|
+
href: '/en',
|
|
132
|
+
hrefLang: 'en',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -21,6 +21,7 @@ import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
|
|
|
21
21
|
import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
22
22
|
import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content';
|
|
23
23
|
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
24
|
+
import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs';
|
|
24
25
|
|
|
25
26
|
import config from '@plone/volto/registry';
|
|
26
27
|
import SlotRenderer from '../SlotRenderer/SlotRenderer';
|
|
@@ -234,6 +235,7 @@ class View extends Component {
|
|
|
234
235
|
return (
|
|
235
236
|
<div id="view" tabIndex="-1">
|
|
236
237
|
<ContentMetadataTags content={this.props.content} />
|
|
238
|
+
<AlternateHrefLangs content={this.props.content} />
|
|
237
239
|
{/* Body class if displayName in component is set */}
|
|
238
240
|
<BodyClass
|
|
239
241
|
className={
|
|
@@ -13,9 +13,13 @@ export default function useClipboard(clipboardText = '') {
|
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const copyAction = useCallback(() => {
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const copyAction = useCallback(async () => {
|
|
17
|
+
try {
|
|
18
|
+
await copyToClipboard(stringToCopy.current);
|
|
19
|
+
setCopied(true);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
setCopied(false);
|
|
22
|
+
}
|
|
19
23
|
}, [stringToCopy]);
|
|
20
24
|
|
|
21
25
|
useEffect(() => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function AlternateHrefLangs(props: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|