@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -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
- <link
7
- rel={'stylesheet'}
8
- href={`${config.settings.apiPath}/voltoCustom.css`}
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={['Value1', 'Value2']}
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(data.url)}/@@images/image`;
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(data.url)}/@@images/image`;
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 (selectedOption?.value === 'createNewVersion' && workflowLoaded) {
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
- React.useEffect(() => {
70
+ // Additional check for token changes
81
71
  if (token !== previousToken) {
82
- const { settings } = config;
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, pathname, previousToken]);
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
- <Header.TopDropdownMenu
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 renderer from 'react-test-renderer';
3
- import { render, fireEvent } from '@testing-library/react';
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: ['en'],
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 component = renderer.create(
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
- const json = component.toJSON();
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: ['en'],
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
- // expect(getByText('da')).toBeInTheDocument();
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: undefined,
149
+ locale: 'en',
171
150
  messages: {},
172
151
  },
173
152
  navigation: {
174
- items: ['en'],
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
- // expect(getByText('da')).toBeInTheDocument();
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: undefined,
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: undefined, title: 'test 2', items: [] },
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
- // expect(getByText('da')).toBeInTheDocument();
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
+ };
@@ -0,0 +1,3 @@
1
+ Customized Image component to restrict the image scales passed to srcset.
2
+ Right now we could select the small size and yet on FHD displays the image served
3
+ would be the huge scale.
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