@plone/volto 18.5.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 +41 -1
- package/cypress/support/commands.js +22 -0
- package/locales/it.json +1 -1
- 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/constants/Languages.cjs +0 -1
- 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
- package/types/constants/Languages.d.cts +0 -1
- package/locales/sv/LC_MESSAGES/volto.po +0 -5193
|
@@ -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 {};
|