@eeacms/volto-eea-website-theme 2.0.2 → 2.1.1
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 +14 -0
- package/package.json +1 -1
- package/src/components/theme/CustomCSS/CustomCSS.jsx +4 -4
- package/src/components/theme/Widgets/TopicsWidget.test.jsx +7 -2
- package/src/customizations/volto/components/manage/Blocks/Image/Edit.jsx +7 -2
- package/src/customizations/volto/components/manage/Blocks/Image/View.jsx +3 -2
- package/src/customizations/volto/components/manage/Blocks/Image/schema.js +2 -0
- package/src/customizations/volto/components/manage/Widgets/InternalUrlWidget.jsx +189 -0
- package/src/customizations/volto/components/manage/Workflow/Workflow.jsx +6 -2
- package/src/customizations/volto/components/theme/Header/Header.jsx +12 -69
- package/src/customizations/volto/components/theme/Header/Header.test.jsx +48 -59
- package/src/customizations/volto/components/theme/Header/LanguageSwitcher.jsx +71 -0
- package/src/customizations/volto/components/theme/Image/Image.jsx +119 -0
- package/src/customizations/volto/components/theme/Image/README.md +3 -0
- package/src/index.js +12 -0
package/CHANGELOG.md
CHANGED
@@ -4,6 +4,20 @@ 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
|
+
### [2.1.1](https://github.com/eea/volto-eea-website-theme/compare/2.1.0...2.1.1) - 28 May 2024
|
8
|
+
|
9
|
+
#### :bug: Bug Fixes
|
10
|
+
|
11
|
+
- fix: wait for the draft version to be created - refs #270058 [dobri1408 - [`d32b0d7`](https://github.com/eea/volto-eea-website-theme/commit/d32b0d7d020ead173c13789936a95c6407bddcd6)]
|
12
|
+
|
13
|
+
#### :hammer_and_wrench: Others
|
14
|
+
|
15
|
+
- Fix InternalUrlWidget.jsx until merged in volto core - refs #269272 [dobri1408 - [`73c383a`](https://github.com/eea/volto-eea-website-theme/commit/73c383a4bf1cc77902601493ad4aa359581f5c2c)]
|
16
|
+
### [2.1.0](https://github.com/eea/volto-eea-website-theme/compare/2.0.2...2.1.0) - 23 May 2024
|
17
|
+
|
18
|
+
#### :hammer_and_wrench: Others
|
19
|
+
|
20
|
+
- bump package version [David Ichim - [`3c0c2eb`](https://github.com/eea/volto-eea-website-theme/commit/3c0c2eb3a863e85451c68f6d56b3e704de260618)]
|
7
21
|
### [2.0.2](https://github.com/eea/volto-eea-website-theme/compare/2.0.1...2.0.2) - 20 May 2024
|
8
22
|
|
9
23
|
### [2.0.1](https://github.com/eea/volto-eea-website-theme/compare/2.0.0...2.0.1) - 15 May 2024
|
package/package.json
CHANGED
@@ -2,11 +2,11 @@ import React from 'react';
|
|
2
2
|
import config from '@plone/volto/registry';
|
3
3
|
|
4
4
|
const CustomCSS = (props) => {
|
5
|
+
const href = `${config.settings.apiPath}/voltoCustom.css`;
|
5
6
|
return (
|
6
|
-
|
7
|
-
rel={
|
8
|
-
|
9
|
-
/>
|
7
|
+
<>
|
8
|
+
<link rel="stylesheet" href={href} />
|
9
|
+
</>
|
10
10
|
);
|
11
11
|
};
|
12
12
|
export default CustomCSS;
|
@@ -18,12 +18,17 @@ describe('TopicsWidget Component', () => {
|
|
18
18
|
},
|
19
19
|
});
|
20
20
|
|
21
|
+
const tags = [
|
22
|
+
{ title: 'Environment', token: '1' },
|
23
|
+
{ title: 'Climate', token: '2' },
|
24
|
+
];
|
25
|
+
|
21
26
|
const { container } = render(
|
22
27
|
<Provider store={store}>
|
23
28
|
<Router history={history}>
|
24
29
|
<TopicsWidget
|
25
|
-
value={
|
26
|
-
children={
|
30
|
+
value={tags}
|
31
|
+
children={(tagTitle) => <span>{tagTitle}</span>}
|
27
32
|
className={'test'}
|
28
33
|
/>
|
29
34
|
</Router>
|
@@ -313,6 +313,7 @@ class Edit extends Component {
|
|
313
313
|
'@id': data.url,
|
314
314
|
image_field: data.image_field,
|
315
315
|
image_scales: data.image_scales,
|
316
|
+
data: data,
|
316
317
|
}
|
317
318
|
: undefined
|
318
319
|
}
|
@@ -323,7 +324,9 @@ class Edit extends Component {
|
|
323
324
|
? // Backwards compat in the case that the block is storing the full server URL
|
324
325
|
(() => {
|
325
326
|
if (data.size === 'l')
|
326
|
-
return `${flattenToAppURL(
|
327
|
+
return `${flattenToAppURL(
|
328
|
+
data.url,
|
329
|
+
)}/@@images/image/large`;
|
327
330
|
if (data.size === 'm')
|
328
331
|
return `${flattenToAppURL(
|
329
332
|
data.url,
|
@@ -332,7 +335,9 @@ class Edit extends Component {
|
|
332
335
|
return `${flattenToAppURL(
|
333
336
|
data.url,
|
334
337
|
)}/@@images/image/mini`;
|
335
|
-
return `${flattenToAppURL(
|
338
|
+
return `${flattenToAppURL(
|
339
|
+
data.url,
|
340
|
+
)}/@@images/image/large`;
|
336
341
|
})()
|
337
342
|
: data.url
|
338
343
|
}
|
@@ -76,6 +76,7 @@ export const View = (props) => {
|
|
76
76
|
'@id': data.url,
|
77
77
|
image_field: data.image_field,
|
78
78
|
image_scales: data.image_scales,
|
79
|
+
data: data,
|
79
80
|
}
|
80
81
|
: undefined
|
81
82
|
}
|
@@ -88,7 +89,7 @@ export const View = (props) => {
|
|
88
89
|
if (data.size === 'l')
|
89
90
|
return `${flattenToAppURL(
|
90
91
|
data.url,
|
91
|
-
)}/@@images/image`;
|
92
|
+
)}/@@images/image/large`;
|
92
93
|
if (data.size === 'm')
|
93
94
|
return `${flattenToAppURL(
|
94
95
|
data.url,
|
@@ -99,7 +100,7 @@ export const View = (props) => {
|
|
99
100
|
)}/@@images/image/mini`;
|
100
101
|
return `${flattenToAppURL(
|
101
102
|
data.url,
|
102
|
-
)}/@@images/image`;
|
103
|
+
)}/@@images/image/large`;
|
103
104
|
})()
|
104
105
|
: data.url
|
105
106
|
}
|
@@ -95,10 +95,12 @@ export function ImageSchema({ formData, intl }) {
|
|
95
95
|
align: {
|
96
96
|
title: intl.formatMessage(messages.Align),
|
97
97
|
widget: 'align',
|
98
|
+
default: 'center',
|
98
99
|
},
|
99
100
|
size: {
|
100
101
|
title: intl.formatMessage(messages.size),
|
101
102
|
widget: 'image_size',
|
103
|
+
default: 'l',
|
102
104
|
},
|
103
105
|
href: {
|
104
106
|
title: intl.formatMessage(messages.LinkTo),
|
@@ -0,0 +1,189 @@
|
|
1
|
+
/**
|
2
|
+
* UrlWidget component.
|
3
|
+
* @module components/manage/Widgets/UrlWidget
|
4
|
+
* Volto pr: https://github.com/plone/volto/pull/6036
|
5
|
+
* Remove after the pr has been merged and put in the right version
|
6
|
+
*/
|
7
|
+
|
8
|
+
import React, { useState, useEffect } from 'react';
|
9
|
+
import PropTypes from 'prop-types';
|
10
|
+
import { Input, Button } from 'semantic-ui-react';
|
11
|
+
import { Icon } from '@plone/volto/components';
|
12
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
13
|
+
import { isInternalURL, flattenToAppURL, URLUtils } from '@plone/volto/helpers';
|
14
|
+
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
|
15
|
+
import clearSVG from '@plone/volto/icons/clear.svg';
|
16
|
+
import navTreeSVG from '@plone/volto/icons/nav.svg';
|
17
|
+
|
18
|
+
/** Widget to edit urls
|
19
|
+
*
|
20
|
+
* This is the default widget used for the `remoteUrl` field. You can also use
|
21
|
+
* it by declaring a field like:
|
22
|
+
*
|
23
|
+
* ```jsx
|
24
|
+
* {
|
25
|
+
* title: "URL",
|
26
|
+
* widget: 'url',
|
27
|
+
* }
|
28
|
+
* ```
|
29
|
+
*/
|
30
|
+
export const InternalUrlWidget = (props) => {
|
31
|
+
const {
|
32
|
+
id,
|
33
|
+
onChange,
|
34
|
+
onBlur,
|
35
|
+
onClick,
|
36
|
+
minLength,
|
37
|
+
maxLength,
|
38
|
+
placeholder,
|
39
|
+
isDisabled,
|
40
|
+
value: propValue,
|
41
|
+
} = props;
|
42
|
+
const inputId = `field-${id}`;
|
43
|
+
|
44
|
+
const [value, setValue] = useState(flattenToAppURL(propValue));
|
45
|
+
const [isInvalid, setIsInvalid] = useState(false);
|
46
|
+
|
47
|
+
useEffect(() => {
|
48
|
+
if (propValue !== value) {
|
49
|
+
setValue(flattenToAppURL(propValue));
|
50
|
+
}
|
51
|
+
}, [propValue, value]);
|
52
|
+
/**
|
53
|
+
* Clear handler
|
54
|
+
* @method clear
|
55
|
+
* @param {Object} value Value
|
56
|
+
* @returns {undefined}
|
57
|
+
*/
|
58
|
+
const clear = () => {
|
59
|
+
setValue('');
|
60
|
+
onChange(id, undefined);
|
61
|
+
};
|
62
|
+
|
63
|
+
const onChangeValue = (_value) => {
|
64
|
+
let newValue = _value;
|
65
|
+
if (newValue?.length > 0) {
|
66
|
+
if (isInvalid && URLUtils.isUrl(URLUtils.normalizeUrl(newValue))) {
|
67
|
+
setIsInvalid(false);
|
68
|
+
}
|
69
|
+
|
70
|
+
if (isInternalURL(newValue)) {
|
71
|
+
newValue = flattenToAppURL(newValue);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
setValue(newValue);
|
76
|
+
|
77
|
+
newValue = isInternalURL(newValue) ? flattenToAppURL(newValue) : newValue;
|
78
|
+
|
79
|
+
if (!isInternalURL(newValue) && newValue.length > 0) {
|
80
|
+
const checkedURL = URLUtils.checkAndNormalizeUrl(newValue);
|
81
|
+
newValue = checkedURL.url;
|
82
|
+
if (!checkedURL.isValid) {
|
83
|
+
setIsInvalid(true);
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
onChange(id, newValue === '' ? undefined : newValue);
|
88
|
+
};
|
89
|
+
|
90
|
+
return (
|
91
|
+
<FormFieldWrapper {...props} className="url wide">
|
92
|
+
<div className="wrapper">
|
93
|
+
<Input
|
94
|
+
id={inputId}
|
95
|
+
name={id}
|
96
|
+
type="url"
|
97
|
+
value={value || ''}
|
98
|
+
disabled={isDisabled}
|
99
|
+
placeholder={placeholder}
|
100
|
+
onChange={({ target }) => onChangeValue(target.value)}
|
101
|
+
onBlur={({ target }) =>
|
102
|
+
onBlur(id, target.value === '' ? undefined : target.value)
|
103
|
+
}
|
104
|
+
onClick={() => onClick()}
|
105
|
+
minLength={minLength || null}
|
106
|
+
maxLength={maxLength || null}
|
107
|
+
error={isInvalid}
|
108
|
+
/>
|
109
|
+
{value?.length > 0 ? (
|
110
|
+
<Button.Group>
|
111
|
+
<Button
|
112
|
+
basic
|
113
|
+
className="cancel"
|
114
|
+
aria-label="clearUrlBrowser"
|
115
|
+
onClick={(e) => {
|
116
|
+
e.preventDefault();
|
117
|
+
e.stopPropagation();
|
118
|
+
clear();
|
119
|
+
}}
|
120
|
+
>
|
121
|
+
<Icon name={clearSVG} size="30px" />
|
122
|
+
</Button>
|
123
|
+
</Button.Group>
|
124
|
+
) : (
|
125
|
+
<Button.Group>
|
126
|
+
<Button
|
127
|
+
basic
|
128
|
+
icon
|
129
|
+
aria-label="openUrlBrowser"
|
130
|
+
onClick={(e) => {
|
131
|
+
e.preventDefault();
|
132
|
+
e.stopPropagation();
|
133
|
+
props.openObjectBrowser({
|
134
|
+
mode: 'link',
|
135
|
+
overlay: true,
|
136
|
+
onSelectItem: (url) => {
|
137
|
+
onChangeValue(url);
|
138
|
+
},
|
139
|
+
});
|
140
|
+
}}
|
141
|
+
>
|
142
|
+
<Icon name={navTreeSVG} size="24px" />
|
143
|
+
</Button>
|
144
|
+
</Button.Group>
|
145
|
+
)}
|
146
|
+
</div>
|
147
|
+
</FormFieldWrapper>
|
148
|
+
);
|
149
|
+
};
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Property types
|
153
|
+
* @property {Object} propTypes Property types.
|
154
|
+
* @static
|
155
|
+
*/
|
156
|
+
InternalUrlWidget.propTypes = {
|
157
|
+
id: PropTypes.string.isRequired,
|
158
|
+
title: PropTypes.string.isRequired,
|
159
|
+
description: PropTypes.string,
|
160
|
+
required: PropTypes.bool,
|
161
|
+
error: PropTypes.arrayOf(PropTypes.string),
|
162
|
+
value: PropTypes.string,
|
163
|
+
onChange: PropTypes.func.isRequired,
|
164
|
+
onBlur: PropTypes.func,
|
165
|
+
onClick: PropTypes.func,
|
166
|
+
minLength: PropTypes.number,
|
167
|
+
maxLength: PropTypes.number,
|
168
|
+
openObjectBrowser: PropTypes.func.isRequired,
|
169
|
+
placeholder: PropTypes.string,
|
170
|
+
};
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Default properties.
|
174
|
+
* @property {Object} defaultProps Default properties.
|
175
|
+
* @static
|
176
|
+
*/
|
177
|
+
InternalUrlWidget.defaultProps = {
|
178
|
+
description: null,
|
179
|
+
required: false,
|
180
|
+
error: [],
|
181
|
+
value: null,
|
182
|
+
onChange: () => {},
|
183
|
+
onBlur: () => {},
|
184
|
+
onClick: () => {},
|
185
|
+
minLength: null,
|
186
|
+
maxLength: null,
|
187
|
+
};
|
188
|
+
|
189
|
+
export default withObjectBrowser(InternalUrlWidget);
|
@@ -258,10 +258,14 @@ const Workflow = (props) => {
|
|
258
258
|
};
|
259
259
|
|
260
260
|
useEffect(() => {
|
261
|
-
if (
|
261
|
+
if (
|
262
|
+
selectedOption?.value === 'createNewVersion' &&
|
263
|
+
workflowLoaded &&
|
264
|
+
loaded
|
265
|
+
) {
|
262
266
|
history.push(`${pathname}.1`);
|
263
267
|
}
|
264
|
-
}, [history, pathname, selectedOption?.value, workflowLoaded]);
|
268
|
+
}, [history, pathname, selectedOption?.value, workflowLoaded, loaded]);
|
265
269
|
|
266
270
|
const { Placeholder } = props.reactSelect.components;
|
267
271
|
const Select = props.reactSelect.default;
|
@@ -9,16 +9,10 @@ import { connect, useDispatch, useSelector } from 'react-redux';
|
|
9
9
|
|
10
10
|
import { withRouter } from 'react-router-dom';
|
11
11
|
import { UniversalLink } from '@plone/volto/components';
|
12
|
-
import {
|
13
|
-
getBaseUrl,
|
14
|
-
hasApiExpander,
|
15
|
-
flattenToAppURL,
|
16
|
-
} from '@plone/volto/helpers';
|
12
|
+
import { getBaseUrl, hasApiExpander } from '@plone/volto/helpers';
|
17
13
|
import { getNavigation } from '@plone/volto/actions';
|
18
14
|
import { Header, Logo } from '@eeacms/volto-eea-design-system/ui';
|
19
15
|
import { usePrevious } from '@eeacms/volto-eea-design-system/helpers';
|
20
|
-
import { find } from 'lodash';
|
21
|
-
import globeIcon from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/global-line.svg';
|
22
16
|
import eeaFlag from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/eea.png';
|
23
17
|
|
24
18
|
import config from '@plone/volto/registry';
|
@@ -26,6 +20,9 @@ import { compose } from 'recompose';
|
|
26
20
|
import { BodyClass } from '@plone/volto/helpers';
|
27
21
|
|
28
22
|
import cx from 'classnames';
|
23
|
+
import loadable from '@loadable/component';
|
24
|
+
|
25
|
+
const LazyLanguageSwitcher = loadable(() => import('./LanguageSwitcher'));
|
29
26
|
|
30
27
|
function removeTrailingSlash(path) {
|
31
28
|
return path.replace(/\/+$/, '');
|
@@ -35,11 +32,6 @@ function removeTrailingSlash(path) {
|
|
35
32
|
* EEA Specific Header component.
|
36
33
|
*/
|
37
34
|
const EEAHeader = ({ pathname, token, items, history, subsite }) => {
|
38
|
-
const currentLang = useSelector((state) => state.intl.locale);
|
39
|
-
const translations = useSelector(
|
40
|
-
(state) => state.content.data?.['@components']?.translations?.items,
|
41
|
-
);
|
42
|
-
|
43
35
|
const router_pathname = useSelector((state) => {
|
44
36
|
return removeTrailingSlash(state.router?.location?.pathname) || '';
|
45
37
|
});
|
@@ -61,31 +53,25 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
|
|
61
53
|
const { eea } = config.settings;
|
62
54
|
const headerOpts = eea.headerOpts || {};
|
63
55
|
const headerSearchBox = eea.headerSearchBox || [];
|
64
|
-
const { logo, logoWhite } = headerOpts
|
56
|
+
const { logo, logoWhite } = headerOpts;
|
65
57
|
const width = useSelector((state) => state.screen?.width);
|
66
58
|
const dispatch = useDispatch();
|
67
59
|
const previousToken = usePrevious(token);
|
68
|
-
const [language, setLanguage] = React.useState(
|
69
|
-
currentLang || eea.defaultLanguage,
|
70
|
-
);
|
71
60
|
|
72
61
|
React.useEffect(() => {
|
73
|
-
const { settings } = config;
|
74
62
|
const base_url = getBaseUrl(pathname);
|
63
|
+
const { settings } = config;
|
64
|
+
|
65
|
+
// Check if navigation data needs to be fetched based on the API expander availability
|
75
66
|
if (!hasApiExpander('navigation', base_url)) {
|
76
67
|
dispatch(getNavigation(base_url, settings.navDepth));
|
77
68
|
}
|
78
|
-
}, [pathname, dispatch]);
|
79
69
|
|
80
|
-
|
70
|
+
// Additional check for token changes
|
81
71
|
if (token !== previousToken) {
|
82
|
-
|
83
|
-
const base = getBaseUrl(pathname);
|
84
|
-
if (!hasApiExpander('navigation', base)) {
|
85
|
-
dispatch(getNavigation(base, settings.navDepth));
|
86
|
-
}
|
72
|
+
dispatch(getNavigation(base_url, settings.navDepth));
|
87
73
|
}
|
88
|
-
}, [token, dispatch,
|
74
|
+
}, [pathname, token, dispatch, previousToken]);
|
89
75
|
|
90
76
|
return (
|
91
77
|
<Header menuItems={items}>
|
@@ -155,50 +141,7 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
|
|
155
141
|
{config.settings.isMultilingual &&
|
156
142
|
config.settings.supportedLanguages.length > 1 &&
|
157
143
|
config.settings.hasLanguageDropdown && (
|
158
|
-
<
|
159
|
-
id="language-switcher"
|
160
|
-
className="item"
|
161
|
-
text={`${language.toUpperCase()}`}
|
162
|
-
mobileText={`${language.toUpperCase()}`}
|
163
|
-
icon={
|
164
|
-
<Image
|
165
|
-
src={globeIcon}
|
166
|
-
alt="language dropdown globe icon"
|
167
|
-
></Image>
|
168
|
-
}
|
169
|
-
viewportWidth={width}
|
170
|
-
>
|
171
|
-
<ul
|
172
|
-
className="wrapper language-list"
|
173
|
-
role="listbox"
|
174
|
-
aria-label="language switcher"
|
175
|
-
>
|
176
|
-
{eea.languages.map((item, index) => (
|
177
|
-
<Dropdown.Item
|
178
|
-
as="li"
|
179
|
-
key={index}
|
180
|
-
text={
|
181
|
-
<span>
|
182
|
-
{item.name}
|
183
|
-
<span className="country-code">
|
184
|
-
{item.code.toUpperCase()}
|
185
|
-
</span>
|
186
|
-
</span>
|
187
|
-
}
|
188
|
-
onClick={() => {
|
189
|
-
const translation = find(translations, {
|
190
|
-
language: item.code,
|
191
|
-
});
|
192
|
-
const to = translation
|
193
|
-
? flattenToAppURL(translation['@id'])
|
194
|
-
: `/${item.code}`;
|
195
|
-
setLanguage(item.code);
|
196
|
-
history.push(to);
|
197
|
-
}}
|
198
|
-
></Dropdown.Item>
|
199
|
-
))}
|
200
|
-
</ul>
|
201
|
-
</Header.TopDropdownMenu>
|
144
|
+
<LazyLanguageSwitcher width={width} history={history} />
|
202
145
|
)}
|
203
146
|
</Header.TopHeader>
|
204
147
|
<Header.Main
|
@@ -1,19 +1,33 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import
|
3
|
-
import
|
2
|
+
import { render, fireEvent, getByText } from '@testing-library/react';
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
4
4
|
import configureStore from 'redux-mock-store';
|
5
5
|
import { Router } from 'react-router-dom';
|
6
6
|
import { createMemoryHistory } from 'history';
|
7
7
|
import { Provider } from 'react-intl-redux';
|
8
8
|
import config from '@plone/volto/registry';
|
9
|
-
|
9
|
+
import { waitFor } from '@testing-library/react';
|
10
10
|
import Header from './Header';
|
11
11
|
|
12
12
|
const mockStore = configureStore();
|
13
13
|
let history = createMemoryHistory();
|
14
14
|
|
15
|
+
const item = {
|
16
|
+
'@id': 'en',
|
17
|
+
description: 'Description of item',
|
18
|
+
items: [],
|
19
|
+
review_state: 'published',
|
20
|
+
title: 'Test english article',
|
21
|
+
};
|
22
|
+
|
23
|
+
jest.mock('@plone/volto/helpers/Loadable/Loadable');
|
24
|
+
beforeAll(
|
25
|
+
async () =>
|
26
|
+
await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
|
27
|
+
);
|
28
|
+
|
15
29
|
describe('Header', () => {
|
16
|
-
it('renders a header component', () => {
|
30
|
+
it('renders a header component with homepage_inverse_view layout', () => {
|
17
31
|
const store = mockStore({
|
18
32
|
userSession: { token: null },
|
19
33
|
intl: {
|
@@ -21,7 +35,7 @@ describe('Header', () => {
|
|
21
35
|
messages: {},
|
22
36
|
},
|
23
37
|
navigation: {
|
24
|
-
items: [
|
38
|
+
items: [item],
|
25
39
|
},
|
26
40
|
content: {
|
27
41
|
data: {
|
@@ -38,22 +52,23 @@ describe('Header', () => {
|
|
38
52
|
config.settings = {
|
39
53
|
...config.settings,
|
40
54
|
eea: {
|
55
|
+
...config.settings.eea,
|
41
56
|
headerOpts: undefined,
|
57
|
+
logoTargetUrl: '/',
|
42
58
|
},
|
43
59
|
};
|
44
60
|
|
45
|
-
const
|
61
|
+
const { container } = render(
|
46
62
|
<Provider store={store}>
|
47
63
|
<Router history={history}>
|
48
64
|
<Header pathname="/home" />
|
49
65
|
</Router>
|
50
66
|
</Provider>,
|
51
67
|
);
|
52
|
-
|
53
|
-
expect(json).toMatchSnapshot();
|
68
|
+
expect(container).toMatchSnapshot();
|
54
69
|
});
|
55
70
|
|
56
|
-
it('renders a header component', () => {
|
71
|
+
it('renders a header component with homepage_view layout and translations', async () => {
|
57
72
|
const store = mockStore({
|
58
73
|
userSession: { token: null },
|
59
74
|
intl: {
|
@@ -61,47 +76,7 @@ describe('Header', () => {
|
|
61
76
|
messages: {},
|
62
77
|
},
|
63
78
|
navigation: {
|
64
|
-
items: [
|
65
|
-
},
|
66
|
-
content: {
|
67
|
-
data: {
|
68
|
-
layout: 'homepage_inverse_view',
|
69
|
-
},
|
70
|
-
},
|
71
|
-
router: {
|
72
|
-
location: {
|
73
|
-
pathname: '/home/',
|
74
|
-
},
|
75
|
-
},
|
76
|
-
});
|
77
|
-
|
78
|
-
config.settings = {
|
79
|
-
...config.settings,
|
80
|
-
eea: {
|
81
|
-
headerOpts: {},
|
82
|
-
},
|
83
|
-
};
|
84
|
-
|
85
|
-
const component = renderer.create(
|
86
|
-
<Provider store={store}>
|
87
|
-
<Router history={history}>
|
88
|
-
<Header pathname="/blog" />
|
89
|
-
</Router>
|
90
|
-
</Provider>,
|
91
|
-
);
|
92
|
-
const json = component.toJSON();
|
93
|
-
expect(json).toMatchSnapshot();
|
94
|
-
});
|
95
|
-
|
96
|
-
it('renders a header component', () => {
|
97
|
-
const store = mockStore({
|
98
|
-
userSession: { token: null },
|
99
|
-
intl: {
|
100
|
-
locale: undefined,
|
101
|
-
messages: {},
|
102
|
-
},
|
103
|
-
navigation: {
|
104
|
-
items: ['en'],
|
79
|
+
items: [item],
|
105
80
|
},
|
106
81
|
content: {
|
107
82
|
data: {
|
@@ -123,6 +98,7 @@ describe('Header', () => {
|
|
123
98
|
config.settings = {
|
124
99
|
...config.settings,
|
125
100
|
eea: {
|
101
|
+
...config.settings.eea,
|
126
102
|
headerOpts: {
|
127
103
|
partnerLinks: {
|
128
104
|
links: [{ href: '/link1', title: 'link 1' }],
|
@@ -145,6 +121,9 @@ describe('Header', () => {
|
|
145
121
|
);
|
146
122
|
|
147
123
|
fireEvent.click(container.querySelector('.content'));
|
124
|
+
await waitFor(() => {
|
125
|
+
expect(container.querySelector('.country-code')).not.toBeNull();
|
126
|
+
});
|
148
127
|
fireEvent.keyDown(container.querySelector('.content'), { keyCode: 37 });
|
149
128
|
fireEvent.keyDown(container.querySelector('.content a'), { keyCode: 37 });
|
150
129
|
fireEvent.keyDown(container.querySelector('a[href="/link1"]'), {
|
@@ -152,7 +131,7 @@ describe('Header', () => {
|
|
152
131
|
});
|
153
132
|
fireEvent.click(container.querySelector('.country-code'));
|
154
133
|
|
155
|
-
|
134
|
+
expect(getByText(container, 'RO')).toBeInTheDocument();
|
156
135
|
|
157
136
|
rerender(
|
158
137
|
<Provider store={{ ...store, userSession: { token: '1234' } }}>
|
@@ -163,15 +142,15 @@ describe('Header', () => {
|
|
163
142
|
);
|
164
143
|
});
|
165
144
|
|
166
|
-
it('renders a header component', () => {
|
145
|
+
it('renders a header component with a subsite', async () => {
|
167
146
|
const store = mockStore({
|
168
147
|
userSession: { token: null },
|
169
148
|
intl: {
|
170
|
-
locale:
|
149
|
+
locale: 'en',
|
171
150
|
messages: {},
|
172
151
|
},
|
173
152
|
navigation: {
|
174
|
-
items: [
|
153
|
+
items: [item],
|
175
154
|
},
|
176
155
|
content: {
|
177
156
|
data: {
|
@@ -179,6 +158,7 @@ describe('Header', () => {
|
|
179
158
|
'@components': {
|
180
159
|
subsite: {
|
181
160
|
'@type': 'Subsite',
|
161
|
+
'@id': 'http://localhost:8080/Plone/subsite',
|
182
162
|
title: 'Home Page',
|
183
163
|
subsite_logo: {
|
184
164
|
scales: {
|
@@ -205,6 +185,7 @@ describe('Header', () => {
|
|
205
185
|
config.settings = {
|
206
186
|
...config.settings,
|
207
187
|
eea: {
|
188
|
+
...config.settings.eea,
|
208
189
|
headerOpts: {
|
209
190
|
partnerLinks: {
|
210
191
|
links: [{ href: '/link1', title: 'link 1' }],
|
@@ -227,6 +208,9 @@ describe('Header', () => {
|
|
227
208
|
);
|
228
209
|
|
229
210
|
fireEvent.click(container.querySelector('.content'));
|
211
|
+
await waitFor(() => {
|
212
|
+
expect(container.querySelector('.country-code')).not.toBeNull();
|
213
|
+
});
|
230
214
|
fireEvent.keyDown(container.querySelector('.content'), { keyCode: 37 });
|
231
215
|
fireEvent.keyDown(container.querySelector('.content a'), { keyCode: 37 });
|
232
216
|
fireEvent.keyDown(container.querySelector('a[href="/link1"]'), {
|
@@ -234,7 +218,7 @@ describe('Header', () => {
|
|
234
218
|
});
|
235
219
|
fireEvent.click(container.querySelector('.country-code'));
|
236
220
|
|
237
|
-
|
221
|
+
expect(getByText(container, 'RO')).toBeInTheDocument();
|
238
222
|
|
239
223
|
rerender(
|
240
224
|
<Provider store={{ ...store, userSession: { token: '1234' } }}>
|
@@ -245,17 +229,17 @@ describe('Header', () => {
|
|
245
229
|
);
|
246
230
|
});
|
247
231
|
|
248
|
-
it('renders a header component', () => {
|
232
|
+
it('renders a header component with a subsite and two children', async () => {
|
249
233
|
const store = mockStore({
|
250
234
|
userSession: { token: null },
|
251
235
|
intl: {
|
252
|
-
locale:
|
236
|
+
locale: 'en',
|
253
237
|
messages: {},
|
254
238
|
},
|
255
239
|
navigation: {
|
256
240
|
items: [
|
257
241
|
{ url: '/test1', title: 'test 1', nav_title: 'Test 1', items: [] },
|
258
|
-
{ url:
|
242
|
+
{ url: '/test2', title: 'test 2', items: [] },
|
259
243
|
],
|
260
244
|
},
|
261
245
|
content: {
|
@@ -264,6 +248,7 @@ describe('Header', () => {
|
|
264
248
|
'@components': {
|
265
249
|
subsite: {
|
266
250
|
'@type': 'Subsite',
|
251
|
+
'@id': 'http://localhost:8080/Plone/subsite',
|
267
252
|
title: 'Home Page',
|
268
253
|
subsite_logo: undefined,
|
269
254
|
},
|
@@ -283,6 +268,7 @@ describe('Header', () => {
|
|
283
268
|
config.settings = {
|
284
269
|
...config.settings,
|
285
270
|
eea: {
|
271
|
+
...config.settings.eea,
|
286
272
|
headerOpts: {
|
287
273
|
partnerLinks: {
|
288
274
|
links: [{ href: '/link1', title: 'link 1' }],
|
@@ -305,6 +291,9 @@ describe('Header', () => {
|
|
305
291
|
);
|
306
292
|
|
307
293
|
fireEvent.click(container.querySelector('.content'));
|
294
|
+
await waitFor(() => {
|
295
|
+
expect(container.querySelector('.country-code')).not.toBeNull();
|
296
|
+
});
|
308
297
|
fireEvent.keyDown(container.querySelector('.content'), { keyCode: 37 });
|
309
298
|
fireEvent.keyDown(container.querySelector('.content a'), { keyCode: 37 });
|
310
299
|
fireEvent.keyDown(container.querySelector('a[href="/link1"]'), {
|
@@ -313,7 +302,7 @@ describe('Header', () => {
|
|
313
302
|
fireEvent.click(container.querySelector('.country-code'));
|
314
303
|
fireEvent.click(container.querySelector('a[href="/test1"]'));
|
315
304
|
|
316
|
-
|
305
|
+
expect(getByText(container, 'RO')).toBeInTheDocument();
|
317
306
|
|
318
307
|
rerender(
|
319
308
|
<Provider store={{ ...store, userSession: { token: '1234' } }}>
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { useSelector } from 'react-redux';
|
3
|
+
import { Dropdown, Image } from 'semantic-ui-react';
|
4
|
+
import { flattenToAppURL } from '@plone/volto/helpers';
|
5
|
+
import { find } from 'lodash';
|
6
|
+
import globeIcon from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/global-line.svg';
|
7
|
+
import config from '@plone/volto/registry';
|
8
|
+
import { Header } from '@eeacms/volto-eea-design-system/ui';
|
9
|
+
|
10
|
+
/**
|
11
|
+
* LanguageSwitcher component.
|
12
|
+
* Provides a dropdown menu for language selection, changing the application's
|
13
|
+
* language and navigating to the corresponding translated URL.
|
14
|
+
*
|
15
|
+
* @param {Object} props - The component props.
|
16
|
+
* @param {number} props.width - The viewport width to adjust the dropdown display.
|
17
|
+
* @param {Object} props.history - The history object from React Router for navigation.
|
18
|
+
*/
|
19
|
+
const LanguageSwitcher = ({ width, history }) => {
|
20
|
+
const currentLang = useSelector((state) => state.intl.locale);
|
21
|
+
const translations = useSelector(
|
22
|
+
(state) => state.content.data?.['@components']?.translations?.items,
|
23
|
+
);
|
24
|
+
const { eea } = config.settings;
|
25
|
+
|
26
|
+
const [language, setLanguage] = React.useState(
|
27
|
+
currentLang || eea.defaultLanguage,
|
28
|
+
);
|
29
|
+
|
30
|
+
return (
|
31
|
+
<Header.TopDropdownMenu
|
32
|
+
id="language-switcher"
|
33
|
+
className="item"
|
34
|
+
text={`${language.toUpperCase()}`}
|
35
|
+
mobileText={`${language.toUpperCase()}`}
|
36
|
+
icon={<Image src={globeIcon} alt="language dropdown globe icon"></Image>}
|
37
|
+
viewportWidth={width}
|
38
|
+
>
|
39
|
+
<ul
|
40
|
+
className="wrapper language-list"
|
41
|
+
role="listbox"
|
42
|
+
aria-label="language switcher"
|
43
|
+
>
|
44
|
+
{eea.languages.map((item, index) => (
|
45
|
+
<Dropdown.Item
|
46
|
+
as="li"
|
47
|
+
key={index}
|
48
|
+
text={
|
49
|
+
<span>
|
50
|
+
{item.name}
|
51
|
+
<span className="country-code">{item.code.toUpperCase()}</span>
|
52
|
+
</span>
|
53
|
+
}
|
54
|
+
onClick={() => {
|
55
|
+
const translation = find(translations, {
|
56
|
+
language: item.code,
|
57
|
+
});
|
58
|
+
const to = translation
|
59
|
+
? flattenToAppURL(translation['@id'])
|
60
|
+
: `/${item.code}`;
|
61
|
+
setLanguage(item.code);
|
62
|
+
history.push(to);
|
63
|
+
}}
|
64
|
+
></Dropdown.Item>
|
65
|
+
))}
|
66
|
+
</ul>
|
67
|
+
</Header.TopDropdownMenu>
|
68
|
+
);
|
69
|
+
};
|
70
|
+
|
71
|
+
export default LanguageSwitcher;
|
@@ -0,0 +1,119 @@
|
|
1
|
+
import PropTypes from 'prop-types';
|
2
|
+
import cx from 'classnames';
|
3
|
+
import { flattenToAppURL, flattenScales } from '@plone/volto/helpers';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Determines the image scale name based on the provided data.
|
7
|
+
*
|
8
|
+
* @param {object} data - The data object containing the image size information.
|
9
|
+
* @param {string} [data.size] - The size of the image, can be 'l', 'm', or 's'.
|
10
|
+
* @returns {string} The name of the image scale, either 'large', 'preview', or 'mini'.
|
11
|
+
*/
|
12
|
+
const imageScaleName = (data) => {
|
13
|
+
if (!data) return 'large';
|
14
|
+
if (data.size === 'l') return 'large';
|
15
|
+
if (data.size === 'm') return 'preview';
|
16
|
+
if (data.size === 's') return 'mini';
|
17
|
+
return 'large';
|
18
|
+
};
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Image component
|
22
|
+
* @param {object} item - Context item that has the image field (can also be a catalog brain or summary)
|
23
|
+
* @param {string} imageField - Key of the image field inside the item, or inside the image_scales object of the item if it is a catalog brain or summary
|
24
|
+
* @param {string} src - URL of the image to be used if the item field is not available
|
25
|
+
* @param {string} alt - Alternative text for the image
|
26
|
+
* @param {boolean} loading - (default: eager) set to `lazy` to lazy load the image
|
27
|
+
* @param {boolean} responsive - (default: false) set to `true` to add the `responsive` class to the image
|
28
|
+
* @param {string} className - Additional classes to add to the image
|
29
|
+
*/
|
30
|
+
export default function Image({
|
31
|
+
item,
|
32
|
+
imageField,
|
33
|
+
src,
|
34
|
+
alt = '',
|
35
|
+
loading = 'eager',
|
36
|
+
responsive = false,
|
37
|
+
className = '',
|
38
|
+
...imageProps
|
39
|
+
}) {
|
40
|
+
if (!item && !src) return null;
|
41
|
+
|
42
|
+
// TypeScript hints for editor autocomplete :)
|
43
|
+
/** @type {React.ImgHTMLAttributes<HTMLImageElement>} */
|
44
|
+
const attrs = {};
|
45
|
+
|
46
|
+
if (!item && src) {
|
47
|
+
attrs.src = src;
|
48
|
+
attrs.className = cx(className, { responsive });
|
49
|
+
} else {
|
50
|
+
const isFromRealObject = !item.image_scales;
|
51
|
+
const imageFieldWithDefault = imageField || item.image_field || 'image';
|
52
|
+
|
53
|
+
const image = isFromRealObject
|
54
|
+
? flattenScales(item['@id'], item[imageFieldWithDefault])
|
55
|
+
: flattenScales(
|
56
|
+
item['@id'],
|
57
|
+
item.image_scales[imageFieldWithDefault]?.[0],
|
58
|
+
);
|
59
|
+
|
60
|
+
if (!image) return null;
|
61
|
+
|
62
|
+
const isSvg = image['content-type'] === 'image/svg+xml';
|
63
|
+
// In case `base_path` is present (`preview_image_link`) use it as base path
|
64
|
+
const basePath = image.base_path || item['@id'];
|
65
|
+
const relativeBasePath = flattenToAppURL(basePath);
|
66
|
+
const selectedScale = imageScaleName(item.data);
|
67
|
+
|
68
|
+
attrs.src = `${relativeBasePath}/${image.download}`;
|
69
|
+
attrs.width = image.width;
|
70
|
+
attrs.height = image.height;
|
71
|
+
attrs.className = cx(className, { responsive });
|
72
|
+
|
73
|
+
if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
|
74
|
+
const filteredScales = [
|
75
|
+
'mini',
|
76
|
+
'preview',
|
77
|
+
'large',
|
78
|
+
item.data?.align === 'full' ? 'huge' : undefined,
|
79
|
+
]
|
80
|
+
.map((key) => image.scales[key])
|
81
|
+
.filter(Boolean);
|
82
|
+
const imageScale = image.scales[selectedScale];
|
83
|
+
if (imageScale) {
|
84
|
+
// set default image size, width and height to the selected scale
|
85
|
+
attrs.width = imageScale.width;
|
86
|
+
attrs.height = imageScale.height;
|
87
|
+
attrs.src = `${relativeBasePath}/${imageScale.download}`;
|
88
|
+
}
|
89
|
+
|
90
|
+
attrs.srcSet = filteredScales
|
91
|
+
.map((scale) => `${relativeBasePath}/${scale.download} ${scale.width}w`)
|
92
|
+
.join(', ');
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
if (loading === 'lazy') {
|
97
|
+
attrs.loading = 'lazy';
|
98
|
+
attrs.decoding = 'async';
|
99
|
+
} else {
|
100
|
+
attrs.fetchpriority = 'high';
|
101
|
+
}
|
102
|
+
|
103
|
+
return <img {...attrs} alt={alt} {...imageProps} />;
|
104
|
+
}
|
105
|
+
|
106
|
+
Image.propTypes = {
|
107
|
+
item: PropTypes.shape({
|
108
|
+
'@id': PropTypes.string,
|
109
|
+
image_field: PropTypes.string,
|
110
|
+
image_scales: PropTypes.object,
|
111
|
+
image: PropTypes.object,
|
112
|
+
}),
|
113
|
+
imageField: PropTypes.string,
|
114
|
+
src: PropTypes.string,
|
115
|
+
alt: PropTypes.string.isRequired,
|
116
|
+
loading: PropTypes.string,
|
117
|
+
responsive: PropTypes.bool,
|
118
|
+
className: PropTypes.string,
|
119
|
+
};
|
package/src/index.js
CHANGED
@@ -194,6 +194,18 @@ const applyConfig = (config) => {
|
|
194
194
|
if (config.blocks.blocksConfig.image) {
|
195
195
|
config.blocks.blocksConfig.image.schemaEnhancer =
|
196
196
|
addStylingFieldsetSchemaEnhancerImagePosition;
|
197
|
+
config.blocks.blocksConfig.image.getSizes = function (data) {
|
198
|
+
if (data.size === 'm' || data.size === 's') return undefined;
|
199
|
+
|
200
|
+
if (data.align === 'left' || data.align === 'right') {
|
201
|
+
if (data.size === 'l') return '400px';
|
202
|
+
if (data.size === 'm') return '200px';
|
203
|
+
if (data.size === 's') return '200px';
|
204
|
+
}
|
205
|
+
if (data.size === 'l') {
|
206
|
+
return '(max-width: 600px) 400px, (max-width: 1440px) 800px, 100vw';
|
207
|
+
}
|
208
|
+
};
|
197
209
|
}
|
198
210
|
|
199
211
|
// Set Languages in nextcloud-video-block
|