@eeacms/volto-cca-policy 0.1.74 → 0.1.75

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,30 @@ 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
+ ### [0.1.75](https://github.com/eea/volto-cca-policy/compare/0.1.74...0.1.75) - 15 February 2024
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: add language dropdown [kreafox - [`22ffe5b`](https://github.com/eea/volto-cca-policy/commit/22ffe5b8664b51fc44e9a86c2387305414e95d62)]
12
+
13
+ #### :bug: Bug Fixes
14
+
15
+ - fix: remove polish language [kreafox - [`29413fd`](https://github.com/eea/volto-cca-policy/commit/29413fdb4f0d97dbdd47a0760972d7b0076e1d45)]
16
+
17
+ #### :nail_care: Enhancements
18
+
19
+ - change: hide language selector from mission [kreafox - [`2f18f6d`](https://github.com/eea/volto-cca-policy/commit/2f18f6d7f6f4961c24e5254dfb431cd28d0bb5cd)]
20
+ - change: remove padding from .content-area [kreafox - [`a456994`](https://github.com/eea/volto-cca-policy/commit/a45699421a3e67e6fbb339934116ae76c62f7912)]
21
+
22
+ #### :hammer_and_wrench: Others
23
+
24
+ - Indicator card - fix error. [GhitaB - [`e917135`](https://github.com/eea/volto-cca-policy/commit/e91713566d7bc3529f740d825e3183322ecb6ecd)]
25
+ - Indicator card - fix missing key. [GhitaB - [`eef5186`](https://github.com/eea/volto-cca-policy/commit/eef51864db680a7c18a9a6875f3ad52a81767dc0)]
26
+ - Fix image src for organisation card. [GhitaB - [`5457713`](https://github.com/eea/volto-cca-policy/commit/5457713795ae28e2c0d475cdea59c5d225d1bd1c)]
27
+ - Add indicator card new listing type for search block in Observatory. [GhitaB - [`09b80dc`](https://github.com/eea/volto-cca-policy/commit/09b80dc0fca8347ee139c5367afae310fa0b2ee3)]
28
+ - Cleanup code [kreafox - [`1908fca`](https://github.com/eea/volto-cca-policy/commit/1908fca7707f526081a449f79eae6bdd6350b886)]
29
+ - Add LanguageSwitch and customized api middleware [Tiberiu Ichim - [`02ba1ca`](https://github.com/eea/volto-cca-policy/commit/02ba1ca72ba2812b51179ad0ac46f37642e6d8c8)]
30
+ - Add pdb button [Tiberiu Ichim - [`18c5630`](https://github.com/eea/volto-cca-policy/commit/18c5630c9c7676ce30ae7e6ac63afc60327e19b9)]
7
31
  ### [0.1.74](https://github.com/eea/volto-cca-policy/compare/0.1.73...0.1.74) - 13 February 2024
8
32
 
9
33
  #### :bug: Bug Fixes
@@ -287,13 +311,10 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
287
311
  - Refs #260715 rast-block wip [Tripon Eugen - [`f19d54e`](https://github.com/eea/volto-cca-policy/commit/f19d54e0b9a6a86bf344eb85b6a1cda7f3de91bf)]
288
312
  - Refs #260715 rast-block wip [Tripon Eugen - [`2828537`](https://github.com/eea/volto-cca-policy/commit/2828537b6c084cd1a82162d552fb4ef025b71f9f)]
289
313
  - Refs #260715 rast-block updates [Tripon Eugen - [`1e803e5`](https://github.com/eea/volto-cca-policy/commit/1e803e5bd3d3fb7558f261c76c68866be7beb8b5)]
290
- - test: [JENKINS] Use java17 for sonarqube scanner [valentinab25 - [`0a15e1b`](https://github.com/eea/volto-cca-policy/commit/0a15e1b2ad081233685e80d5b3c60a8663f6b896)]
291
- - test: [JENKINS] Run cypress in started frontend container [valentinab25 - [`9554e44`](https://github.com/eea/volto-cca-policy/commit/9554e44c92a621a52b2adb5a4830fb084ee5734b)]
292
314
  ### [0.1.49](https://github.com/eea/volto-cca-policy/compare/0.1.48...0.1.49) - 15 November 2023
293
315
 
294
316
  #### :house: Internal changes
295
317
 
296
- - chore: [JENKINS] Refactor automated testing [valentinab25 - [`7b820a6`](https://github.com/eea/volto-cca-policy/commit/7b820a6369c2ddd5203b1a4abe352cb4bb43db7a)]
297
318
  - chore: husky, lint-staged use fixed versions [valentinab25 - [`f0a8061`](https://github.com/eea/volto-cca-policy/commit/f0a8061c275c236deb00087c23fac9860a073106)]
298
319
 
299
320
  #### :hammer_and_wrench: Others
@@ -310,9 +331,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
310
331
  - Refs #259267 - jenkins test [Tripon Eugen - [`cacd31e`](https://github.com/eea/volto-cca-policy/commit/cacd31e7b1afe0983674ed5c7632d2e1d7fa752e)]
311
332
  - Refs #259267 - jenkins [Tripon Eugen - [`5b3affe`](https://github.com/eea/volto-cca-policy/commit/5b3affee8401239de10097884c1b7f2349d15ec0)]
312
333
  - Refs #259267 - add When, lead image and title to files [Tripon Eugen - [`2cedb23`](https://github.com/eea/volto-cca-policy/commit/2cedb237f898af9057e13fba94b615ef71077204)]
313
- - test: [JENKINS] Add cpu limit on cypress docker [valentinab25 - [`4d607a5`](https://github.com/eea/volto-cca-policy/commit/4d607a576e9d0a5c34e48c41b409e7df616ee3d6)]
314
- - test: [JENKINS] Increase shm-size to cypress docker [valentinab25 - [`b7f74d5`](https://github.com/eea/volto-cca-policy/commit/b7f74d53513a6edbfbca5cb6d19687929bb1e5db)]
315
- - test: [JENKINS] Improve cypress time [valentinab25 - [`db65617`](https://github.com/eea/volto-cca-policy/commit/db656173391f65157098d95d388c25f6429753d8)]
316
334
  - Refs #259267 - cca event blocks attachments and check not mandatoty fields [Tripon Eugen - [`3138e5a`](https://github.com/eea/volto-cca-policy/commit/3138e5afb5bfbdbed14e27ed457b16867b7fa414)]
317
335
  - Refs #256681 - Fix error in CCA Event view menu. ([React Intl] An id must be provided to format a message.) [GhitaB - [`517eeb8`](https://github.com/eea/volto-cca-policy/commit/517eeb817264a47bbfd6b9b7d22aaf22d44ed224)]
318
336
  - Refs #161485 - Fix ECDE name conflict. [GhitaB - [`8bfd99f`](https://github.com/eea/volto-cca-policy/commit/8bfd99ff68bb82a04d1c0ed625fa514fcf46289e)]
@@ -529,7 +547,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
529
547
 
530
548
  #### :house: Internal changes
531
549
 
532
- - chore: [JENKINS] Remove alpha testing version [valentinab25 - [`ad1ced0`](https://github.com/eea/volto-cca-policy/commit/ad1ced0971ba116c13a3b5fcc039172cc915c919)]
533
550
 
534
551
  #### :hammer_and_wrench: Others
535
552
 
@@ -1010,7 +1027,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
1010
1027
  #### :hammer_and_wrench: Others
1011
1028
 
1012
1029
  - Refs #158294 - Update supported languages list. [GhitaB - [`0a4f91f`](https://github.com/eea/volto-cca-policy/commit/0a4f91f39b7edc367bd4c127d6a8f273c7788361)]
1013
- - Add Sonarqube tag using cca-frontend addons list [EEA Jenkins - [`8f1f9ce`](https://github.com/eea/volto-cca-policy/commit/8f1f9ce6c22805670cc0800d3c779b6d619d0f31)]
1014
1030
  ### [0.1.1](https://github.com/eea/volto-cca-policy/compare/0.1.0...0.1.1) - 13 December 2022
1015
1031
 
1016
1032
  #### :hammer_and_wrench: Others
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -7,20 +7,22 @@ function MigrationButtons(props) {
7
7
  const show = !!token && contentId && contentId.indexOf('europa.eu') === -1;
8
8
  const base = getBaseUrl(pathname);
9
9
 
10
- const handleClickMigrate = () => {
11
- window.open(`http://localhost:8080/cca/${base}/@@volto_migrate`, '_blank');
12
- };
13
-
14
- const handleClickView = () => {
15
- window.open(`https://climate-adapt.eea.europa.eu${base}`, '_blank');
16
- };
17
-
18
10
  return show ? (
19
- <Plug pluggable="main.toolbar.top" id="cca-migration-helpers" order={0}>
11
+ <Plug
12
+ pluggable="main.toolbar.top"
13
+ id="cca-migration-helpers"
14
+ order={0}
15
+ dependencies={[contentId]}
16
+ >
20
17
  <button
21
18
  className={`circle-right-btn `}
22
19
  id="toolbar-migration"
23
- onClick={handleClickMigrate}
20
+ onClick={() =>
21
+ window.open(
22
+ `http://localhost:8080/cca/${base}/@@volto_migrate`,
23
+ '_blank',
24
+ )
25
+ }
24
26
  title="Migrate context"
25
27
  >
26
28
  M
@@ -29,11 +31,22 @@ function MigrationButtons(props) {
29
31
  <button
30
32
  className={`circle-right-btn `}
31
33
  id="toolbar-view"
32
- onClick={handleClickView}
34
+ onClick={() =>
35
+ window.open(`https://climate-adapt.eea.europa.eu${base}`, '_blank')
36
+ }
33
37
  title="View original"
34
38
  >
35
39
  V
36
40
  </button>
41
+
42
+ <button
43
+ className={`circle-right-btn`}
44
+ id="toolbar-migration"
45
+ onClick={() => window.open(`http://localhost:8080/cca/${base}/@@gopdb`)}
46
+ title="Migrate context"
47
+ >
48
+ PDB
49
+ </button>
37
50
  </Plug>
38
51
  ) : null;
39
52
  }
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { connect } from 'react-redux';
4
+ import { compose } from 'redux';
5
+ import cx from 'classnames';
6
+ import { ConditionalLink } from '@plone/volto/components';
7
+ import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
8
+ import './styles.less';
9
+
10
+ const fixedTitles = {
11
+ C3S: 'Copernicus (C3S)',
12
+ 'Lancet Countdown': 'Lancet Countdown in Europe',
13
+ };
14
+
15
+ const fixTitle = (title) => {
16
+ return fixedTitles[title] || title;
17
+ };
18
+
19
+ const IndicatorCardsListingView = ({ items, isEditMode, token }) => {
20
+ return (
21
+ <div className={cx('ui fluid indicatorCards')}>
22
+ {items.map((item, index) => (
23
+ <div
24
+ className={cx('u-item listing-item simple-listing-item')}
25
+ key={item['@id']}
26
+ >
27
+ <div className="wrapper">
28
+ <div className="slot-top">
29
+ <ConditionalLink
30
+ to={flattenToAppURL(getBaseUrl(item['@id']))}
31
+ condition={!isEditMode}
32
+ >
33
+ <div className="listing-body">
34
+ <h4 className={'listing-header'}>
35
+ {item.title ? item.title : item.id}
36
+ </h4>
37
+ </div>
38
+ </ConditionalLink>
39
+ </div>
40
+ <div className="simple-item-meta">
41
+ <span className="text-left year">
42
+ {item?.cca_published &&
43
+ new Date(item?.publication_date).getFullYear()}
44
+ </span>
45
+ <span className="text-left">
46
+ {item &&
47
+ item.origin_website &&
48
+ item.origin_website.length > 0 &&
49
+ fixTitle(item?.origin_website[0]?.title)}
50
+ </span>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ ))}
55
+ </div>
56
+ );
57
+ };
58
+
59
+ IndicatorCardsListingView.propTypes = {
60
+ items: PropTypes.arrayOf(PropTypes.any).isRequired,
61
+ isEditMode: PropTypes.bool,
62
+ };
63
+
64
+ export default compose(
65
+ connect((state) => ({
66
+ token: state.userSession.token,
67
+ })),
68
+ )(IndicatorCardsListingView);
@@ -52,7 +52,7 @@ const OrganisationCardsListingView = ({ items, isEditMode, token }) => {
52
52
  <div className="header">
53
53
  <a className="image" href={observatoryURL(item)}>
54
54
  <img
55
- src={observatoryURL(item) + '/@@images/logo'}
55
+ src={item['@id'] + '/@@images/logo/preview'}
56
56
  alt={item.title}
57
57
  className="ui image"
58
58
  ></img>
@@ -1,4 +1,5 @@
1
1
  import OrganisationCardsListingView from './OrganisationCardsListingView';
2
+ import IndicatorCardsListingView from './IndicatorCardsListingView';
2
3
 
3
4
  export default function installListing(config) {
4
5
  config.blocks.blocksConfig.listing = {
@@ -12,6 +13,13 @@ export default function installListing(config) {
12
13
  isDefault: false,
13
14
  fullobjects: true,
14
15
  },
16
+ {
17
+ id: 'indicatorCards',
18
+ title: 'Indicator Cards',
19
+ template: IndicatorCardsListingView,
20
+ isDefault: false,
21
+ fullobjects: true,
22
+ },
15
23
  ],
16
24
  };
17
25
 
@@ -1,7 +1,6 @@
1
1
  // Organisation Cards Listing
2
2
  div.organisationCards {
3
3
  a.org-name {
4
- margin-top: 0.5em;
5
4
  font-size: 0.7em;
6
5
  text-transform: uppercase;
7
6
  }
@@ -10,3 +9,22 @@ div.organisationCards {
10
9
  font-size: 0.7em;
11
10
  }
12
11
  }
12
+
13
+ // Indicators Cards Listing
14
+ div.indicatorCards {
15
+ a {
16
+ color: #006bb8 !important;
17
+ }
18
+
19
+ h4 {
20
+ font-weight: 400;
21
+ }
22
+
23
+ span.year {
24
+ margin-right: 1em;
25
+ }
26
+
27
+ .simple-listing-item {
28
+ margin-bottom: 1em !important;
29
+ }
30
+ }
@@ -17,14 +17,13 @@ import {
17
17
  import { getNavigation } from '@plone/volto/actions';
18
18
  import { Header, Logo } from '@eeacms/volto-eea-design-system/ui';
19
19
  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
20
  import eeaFlag from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/eea.png';
23
21
  import { toPublicURL } from '@plone/volto/helpers';
24
22
 
25
23
  import config from '@plone/volto/registry';
26
24
  import { compose } from 'recompose';
27
25
  import { BodyClass } from '@plone/volto/helpers';
26
+ import LanguageSwitch from './LanguageSwitch';
28
27
 
29
28
  import cx from 'classnames';
30
29
 
@@ -63,12 +62,8 @@ const DirectLinkLogo = ({
63
62
  /**
64
63
  * EEA Specific Header component.
65
64
  */
66
- const EEAHeader = ({ pathname, token, items, history, subsite }) => {
67
- const currentLang = useSelector((state) => state.intl.locale);
68
- const translations = useSelector(
69
- (state) => state.content.data?.['@components']?.translations?.items,
70
- );
71
-
65
+ const EEAHeader = (props) => {
66
+ const { pathname, token, items, subsite } = props;
72
67
  const router_pathname = useSelector((state) => {
73
68
  return removeTrailingSlash(state.router?.location?.pathname) || '';
74
69
  });
@@ -94,9 +89,6 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
94
89
  const width = useSelector((state) => state.screen?.width);
95
90
  const dispatch = useDispatch();
96
91
  const previousToken = usePrevious(token);
97
- const [language, setLanguage] = React.useState(
98
- currentLang || eea.defaultLanguage,
99
- );
100
92
 
101
93
  React.useEffect(() => {
102
94
  const { settings } = config;
@@ -184,53 +176,7 @@ const EEAHeader = ({ pathname, token, items, history, subsite }) => {
184
176
  </Header.TopItem>
185
177
  )}
186
178
 
187
- {config.settings.isMultilingual && (
188
- <Header.TopDropdownMenu
189
- id="language-switcher"
190
- className="item"
191
- hasLanguageDropdown={
192
- config.settings.supportedLanguages.length > 1 &&
193
- config.settings.hasLanguageDropdown
194
- }
195
- text={`${language.toUpperCase()}`}
196
- mobileText={`${language.toUpperCase()}`}
197
- icon={
198
- <Image src={globeIcon} alt="language dropdown globe icon"></Image>
199
- }
200
- viewportWidth={width}
201
- >
202
- <ul
203
- className="wrapper language-list"
204
- role="listbox"
205
- aria-label="language switcher"
206
- >
207
- {eea.languages.map((item, index) => (
208
- <Dropdown.Item
209
- as="li"
210
- key={index}
211
- text={
212
- <span>
213
- {item.name}
214
- <span className="country-code">
215
- {item.code.toUpperCase()}
216
- </span>
217
- </span>
218
- }
219
- onClick={() => {
220
- const translation = find(translations, {
221
- language: item.code,
222
- });
223
- const to = translation
224
- ? flattenToAppURL(translation['@id'])
225
- : `/${item.code}`;
226
- setLanguage(item.code);
227
- history.push(to);
228
- }}
229
- ></Dropdown.Item>
230
- ))}
231
- </ul>
232
- </Header.TopDropdownMenu>
233
- )}
179
+ {config.settings.isMultilingual && <LanguageSwitch {...props} />}
234
180
  </Header.TopHeader>
235
181
  <Header.Main
236
182
  pathname={pathname}
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Header } from '@eeacms/volto-eea-design-system/ui';
3
+ import { find } from 'lodash';
4
+ import globeIcon from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/global-line.svg';
5
+ import { useSelector } from 'react-redux';
6
+ import config from '@plone/volto/registry';
7
+ import { Dropdown, Image } from 'semantic-ui-react';
8
+ import { flattenToAppURL } from '@plone/volto/helpers';
9
+
10
+ // dispatch(changeLanguage(redirectToLanguage, locale.default));
11
+ export default function LanguageSwitch({ history }) {
12
+ const { eea } = config.settings;
13
+ const translations = useSelector(
14
+ (state) => state.content.data?.['@components']?.translations?.items,
15
+ );
16
+ const width = useSelector((state) => state.screen?.width);
17
+
18
+ const currentLang = useSelector((state) => state.intl.locale);
19
+ const [language, setLanguage] = React.useState(
20
+ currentLang || eea.defaultLanguage,
21
+ );
22
+
23
+ return (
24
+ <Header.TopDropdownMenu
25
+ id="language-switcher"
26
+ className="item"
27
+ hasLanguageDropdown={
28
+ config.settings.supportedLanguages.length > 1 &&
29
+ config.settings.hasLanguageDropdown
30
+ }
31
+ text={`${language.toUpperCase()}`}
32
+ mobileText={`${language.toUpperCase()}`}
33
+ icon={<Image src={globeIcon} alt="language dropdown globe icon"></Image>}
34
+ viewportWidth={width}
35
+ >
36
+ <ul
37
+ className="wrapper language-list"
38
+ role="listbox"
39
+ aria-label="language switcher"
40
+ >
41
+ {eea.languages.map((item, index) => (
42
+ <Dropdown.Item
43
+ as="li"
44
+ key={index}
45
+ text={
46
+ <span>
47
+ {item.name}
48
+ <span className="country-code">{item.code.toUpperCase()}</span>
49
+ </span>
50
+ }
51
+ onClick={() => {
52
+ const translation = find(translations, {
53
+ language: item.code,
54
+ });
55
+ const to = translation
56
+ ? flattenToAppURL(translation['@id'])
57
+ : `/${item.code}`;
58
+ setLanguage(item.code);
59
+ history.push(to);
60
+ }}
61
+ ></Dropdown.Item>
62
+ ))}
63
+ </ul>
64
+ </Header.TopDropdownMenu>
65
+ );
66
+ }
@@ -52,7 +52,7 @@ const TopDropdownMenu = ({
52
52
  children,
53
53
  className,
54
54
  icon,
55
- hasLanguageDropdown = false,
55
+ hasLanguageDropdown = true,
56
56
  id,
57
57
  tabletText,
58
58
  mobileText,
@@ -0,0 +1,3 @@
1
+ Copy from Volto 16
2
+
3
+ Customized because in CCA the language field in the result is not a value from a vocabulary (is missing token).
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Api middleware.
3
+ * @module middleware/api
4
+ */
5
+
6
+ import Cookies from 'universal-cookie';
7
+ import jwtDecode from 'jwt-decode';
8
+ import { compact, flatten, union } from 'lodash';
9
+ import { matchPath } from 'react-router';
10
+ import qs from 'query-string';
11
+
12
+ import config from '@plone/volto/registry';
13
+
14
+ import {
15
+ GET_CONTENT,
16
+ LOGIN,
17
+ RESET_APIERROR,
18
+ SET_APIERROR,
19
+ } from '@plone/volto/constants/ActionTypes';
20
+ import { changeLanguage } from '@plone/volto/actions';
21
+ import {
22
+ toGettextLang,
23
+ toReactIntlLang,
24
+ getCookieOptions,
25
+ } from '@plone/volto/helpers';
26
+ let socket = null;
27
+
28
+ /**
29
+ *
30
+ * Add configured expanders to an api call for an action
31
+ * Requirements:
32
+ *
33
+ * - It should add the expanders set in the config settings
34
+ * - It should preserve any query if present
35
+ * - It should preserve (and add) any expand parameter (if present) e.g. translations
36
+ * - It should take use the correct codification for arrays in querystring (repeated parameter for each member of the array)
37
+ *
38
+ * @function addExpandersToPath
39
+ * @param {string} path The url/path including the querystring
40
+ * @param {*} type The action type
41
+ * @returns {string} The url/path with the configured expanders added to the query string
42
+ */
43
+ export function addExpandersToPath(path, type, isAnonymous) {
44
+ const { settings } = config;
45
+ const { apiExpanders = [] } = settings;
46
+
47
+ const {
48
+ url,
49
+ query: { expand, ...query },
50
+ } = qs.parseUrl(path, { decode: false });
51
+
52
+ const expandersFromConfig = apiExpanders
53
+ .filter((expand) => matchPath(url, expand.match) && expand[type])
54
+ .map((expand) => expand[type]);
55
+
56
+ const expandMerge = compact(
57
+ union([expand, ...flatten(expandersFromConfig)]),
58
+ ).filter((item) => !(item === 'types' && isAnonymous)); // Remove types expander if isAnonymous
59
+
60
+ const stringifiedExpand = qs.stringify(
61
+ { expand: expandMerge },
62
+ {
63
+ arrayFormat: 'comma',
64
+ encode: false,
65
+ },
66
+ );
67
+
68
+ const querystringFromConfig = apiExpanders
69
+ .filter((expand) => matchPath(url, expand.match) && expand[type])
70
+ .reduce((acc, expand) => ({ ...acc, ...expand?.['querystring'] }), {});
71
+
72
+ const queryMerge = { ...query, ...querystringFromConfig };
73
+
74
+ const stringifiedQuery = qs.stringify(queryMerge, {
75
+ encode: false,
76
+ });
77
+
78
+ if (stringifiedQuery && stringifiedExpand) {
79
+ return `${url}?${stringifiedExpand}&${stringifiedQuery}`;
80
+ } else if (!stringifiedQuery && stringifiedExpand) {
81
+ return `${url}?${stringifiedExpand}`;
82
+ } else if (stringifiedQuery && !stringifiedExpand) {
83
+ return `${url}?${stringifiedQuery}`;
84
+ } else {
85
+ return url;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Send a message on a websocket.
91
+ * @function sendOnSocket
92
+ * @param {Object} request Request object.
93
+ * @returns {Promise} message is send
94
+ */
95
+ function sendOnSocket(request) {
96
+ return new Promise((resolve, reject) => {
97
+ switch (socket.readyState) {
98
+ case socket.CONNECTING:
99
+ socket.addEventListener('open', () => resolve(socket));
100
+ socket.addEventListener('error', reject);
101
+ break;
102
+ case socket.OPEN:
103
+ resolve(socket);
104
+ break;
105
+ default:
106
+ reject();
107
+ break;
108
+ }
109
+ }).then(() => {
110
+ socket.send(JSON.stringify(request));
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Api middleware.
116
+ * @function
117
+ * @param {Object} api Api object.
118
+ * @returns {Promise} Action promise.
119
+ */
120
+ const apiMiddlewareFactory = (api) => ({ dispatch, getState }) => (next) => (
121
+ action,
122
+ ) => {
123
+ const { settings } = config;
124
+
125
+ const isAnonymous = !getState().userSession.token;
126
+
127
+ if (typeof action === 'function') {
128
+ return action(dispatch, getState);
129
+ }
130
+
131
+ const { request, type, mode = 'parallel', ...rest } = action;
132
+ const { subrequest } = action; // We want subrequest remains in `...rest` above
133
+
134
+ let actionPromise;
135
+
136
+ if (!request) {
137
+ return next(action);
138
+ }
139
+
140
+ next({ ...rest, type: `${type}_PENDING` });
141
+
142
+ if (socket) {
143
+ actionPromise = Array.isArray(request)
144
+ ? Promise.all(
145
+ request.map((item) =>
146
+ sendOnSocket({
147
+ ...item,
148
+ path: addExpandersToPath(item.path, type, isAnonymous),
149
+ id: type,
150
+ }),
151
+ ),
152
+ )
153
+ : sendOnSocket({
154
+ ...request,
155
+ path: addExpandersToPath(request.path, type, isAnonymous),
156
+ id: type,
157
+ });
158
+ } else {
159
+ actionPromise = Array.isArray(request)
160
+ ? mode === 'serial'
161
+ ? request.reduce((prevPromise, item) => {
162
+ return prevPromise.then((acc) => {
163
+ return api[item.op](
164
+ addExpandersToPath(item.path, type, isAnonymous),
165
+ {
166
+ data: item.data,
167
+ type: item.type,
168
+ headers: item.headers,
169
+ params: request.params,
170
+ checkUrl: settings.actions_raising_api_errors.includes(
171
+ action.type,
172
+ ),
173
+ },
174
+ ).then((reqres) => {
175
+ return [...acc, reqres];
176
+ });
177
+ });
178
+ }, Promise.resolve([]))
179
+ : Promise.all(
180
+ request.map((item) =>
181
+ api[item.op](addExpandersToPath(item.path, type, isAnonymous), {
182
+ data: item.data,
183
+ type: item.type,
184
+ headers: item.headers,
185
+ params: request.params,
186
+ checkUrl: settings.actions_raising_api_errors.includes(
187
+ action.type,
188
+ ),
189
+ }),
190
+ ),
191
+ )
192
+ : api[request.op](addExpandersToPath(request.path, type, isAnonymous), {
193
+ data: request.data,
194
+ type: request.type,
195
+ headers: request.headers,
196
+ params: request.params,
197
+ checkUrl: settings.actions_raising_api_errors.includes(action.type),
198
+ });
199
+ actionPromise.then(
200
+ (result) => {
201
+ const { settings } = config;
202
+ if (getState().apierror.connectionRefused) {
203
+ next({
204
+ ...rest,
205
+ type: RESET_APIERROR,
206
+ });
207
+ }
208
+ if (type === GET_CONTENT) {
209
+ // customization original: result?.language?.token
210
+ const lang = result?.language?.token || result.language || '';
211
+ // end customization
212
+ if (
213
+ lang &&
214
+ getState().intl.locale !== toReactIntlLang(lang) &&
215
+ !subrequest &&
216
+ config.settings.supportedLanguages.includes(lang)
217
+ ) {
218
+ const langFileName = toGettextLang(lang);
219
+ import('~/../locales/' + langFileName + '.json').then((locale) => {
220
+ dispatch(changeLanguage(lang, locale.default));
221
+ });
222
+ }
223
+ }
224
+ if (type === LOGIN && settings.websockets) {
225
+ const cookies = new Cookies();
226
+ cookies.set(
227
+ 'auth_token',
228
+ result.token,
229
+ getCookieOptions({
230
+ expires: new Date(jwtDecode(result.token).exp * 1000),
231
+ }),
232
+ );
233
+ api.get('/@wstoken').then((res) => {
234
+ socket = new WebSocket(
235
+ `${settings.apiPath.replace('http', 'ws')}/@ws?ws_token=${
236
+ res.token
237
+ }`,
238
+ );
239
+ socket.onmessage = (message) => {
240
+ const packet = JSON.parse(message.data);
241
+ if (packet.error) {
242
+ dispatch({
243
+ type: `${packet.id}_FAIL`,
244
+ error: packet.error,
245
+ });
246
+ } else {
247
+ dispatch({
248
+ type: `${packet.id}_SUCCESS`,
249
+ result: JSON.parse(packet.data),
250
+ });
251
+ }
252
+ };
253
+ });
254
+ }
255
+ try {
256
+ return next({ ...rest, result, type: `${type}_SUCCESS` });
257
+ } catch (error) {
258
+ // There was an exception while processing reducers or downstream middleware.
259
+ next({
260
+ ...rest,
261
+ error: { status: 500, error },
262
+ type: `${type}_FAIL`,
263
+ });
264
+ // Rethrow the original exception on the client side only,
265
+ // so it doesn't fall through to express on the server.
266
+ if (__CLIENT__) throw error;
267
+ }
268
+ },
269
+ (error) => {
270
+ // Only SSR can set ECONNREFUSED
271
+ if (error.code === 'ECONNREFUSED') {
272
+ next({
273
+ ...rest,
274
+ error,
275
+ statusCode: error.code,
276
+ connectionRefused: true,
277
+ type: SET_APIERROR,
278
+ });
279
+ }
280
+
281
+ // Response error is marked crossDomain if CORS error happen
282
+ else if (error.crossDomain) {
283
+ next({
284
+ ...rest,
285
+ error,
286
+ statusCode: 'CORSERROR',
287
+ connectionRefused: false,
288
+ type: SET_APIERROR,
289
+ });
290
+ }
291
+
292
+ // Check for actions who can raise api errors
293
+ if (settings.actions_raising_api_errors.includes(action.type)) {
294
+ // Gateway timeout
295
+ if (error?.response?.statusCode === 504) {
296
+ next({
297
+ ...rest,
298
+ error,
299
+ statusCode: error.code,
300
+ connectionRefused: true,
301
+ type: SET_APIERROR,
302
+ });
303
+ }
304
+
305
+ // Redirect
306
+ else if (error?.code === 301) {
307
+ next({
308
+ ...rest,
309
+ error,
310
+ statusCode: error.code,
311
+ connectionRefused: false,
312
+ type: SET_APIERROR,
313
+ });
314
+ }
315
+
316
+ // Redirect
317
+ else if (error?.code === 408) {
318
+ next({
319
+ ...rest,
320
+ error,
321
+ statusCode: error.code,
322
+ connectionRefused: false,
323
+ type: SET_APIERROR,
324
+ });
325
+ }
326
+
327
+ // Unauthorized
328
+ else if (error?.response?.statusCode === 401) {
329
+ next({
330
+ ...rest,
331
+ error,
332
+ statusCode: error.response,
333
+ message: error.response.body.message,
334
+ connectionRefused: false,
335
+ type: SET_APIERROR,
336
+ });
337
+ }
338
+ }
339
+ return next({ ...rest, error, type: `${type}_FAIL` });
340
+ },
341
+ );
342
+ }
343
+
344
+ return actionPromise;
345
+ };
346
+
347
+ export default apiMiddlewareFactory;
@@ -0,0 +1,354 @@
1
+ /* Original: https://github.com/plone/volto/blob/16.x.x/src/server.jsx */
2
+ /* Line: 59 - Fix crash when a supported language it's not in volto/locales folder */
3
+
4
+ /* eslint no-console: 0 */
5
+ import '@plone/volto/config'; // This is the bootstrap for the global config - server side
6
+ import { existsSync, lstatSync, readFileSync } from 'fs';
7
+ import React from 'react';
8
+ import { StaticRouter } from 'react-router-dom';
9
+ import { Provider } from 'react-intl-redux';
10
+ import express from 'express';
11
+ import { renderToString } from 'react-dom/server';
12
+ import { createMemoryHistory } from 'history';
13
+ import { parse as parseUrl } from 'url';
14
+ import { keys } from 'lodash';
15
+ import locale from 'locale';
16
+ import { detect } from 'detect-browser';
17
+ import path from 'path';
18
+ import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server';
19
+ import { resetServerContext } from 'react-beautiful-dnd';
20
+ import { CookiesProvider } from 'react-cookie';
21
+ import cookiesMiddleware from 'universal-cookie-express';
22
+ import debug from 'debug';
23
+
24
+ import routes from '@root/routes';
25
+ import config from '@plone/volto/registry';
26
+
27
+ import {
28
+ flattenToAppURL,
29
+ Html,
30
+ Api,
31
+ persistAuthToken,
32
+ toBackendLang,
33
+ toGettextLang,
34
+ toReactIntlLang,
35
+ } from '@plone/volto/helpers';
36
+ import { changeLanguage } from '@plone/volto/actions';
37
+
38
+ import userSession from '@plone/volto/reducers/userSession/userSession';
39
+
40
+ import ErrorPage from '@plone/volto/error';
41
+
42
+ import languages from '@plone/volto/constants/Languages';
43
+
44
+ import configureStore from '@plone/volto/store';
45
+ import {
46
+ ReduxAsyncConnect,
47
+ loadOnServer,
48
+ } from '@plone/volto/helpers/AsyncConnect';
49
+
50
+ let locales = {};
51
+
52
+ if (config.settings) {
53
+ config.settings.supportedLanguages.forEach((lang) => {
54
+ const langFileName = toGettextLang(lang);
55
+ import('@root/../locales/' + langFileName + '.json')
56
+ .then((locale) => {
57
+ locales = { ...locales, [toReactIntlLang(lang)]: locale.default };
58
+ })
59
+ // start customization
60
+ .catch((error) => {
61
+ locales = { ...locales, [toReactIntlLang(lang)]: {} };
62
+ });
63
+ // end customization
64
+ });
65
+ }
66
+
67
+ function reactIntlErrorHandler(error) {
68
+ debug('i18n')(error);
69
+ }
70
+
71
+ const supported = new locale.Locales(keys(languages), 'en');
72
+
73
+ const server = express()
74
+ .disable('x-powered-by')
75
+ .head('/*', function (req, res) {
76
+ // Support for HEAD requests. Required by start-test utility in CI.
77
+ res.send('');
78
+ })
79
+ .use(cookiesMiddleware());
80
+
81
+ const middleware = (config.settings.expressMiddleware || []).filter((m) => m);
82
+
83
+ server.all('*', setupServer);
84
+ if (middleware.length) server.use('/', middleware);
85
+
86
+ server.use(function (err, req, res, next) {
87
+ if (err) {
88
+ const { store } = res.locals;
89
+ const errorPage = (
90
+ <Provider store={store} onError={reactIntlErrorHandler}>
91
+ <StaticRouter context={{}} location={req.url}>
92
+ <ErrorPage message={err.message} />
93
+ </StaticRouter>
94
+ </Provider>
95
+ );
96
+
97
+ res.set({
98
+ 'Cache-Control': 'public, max-age=60, no-transform',
99
+ });
100
+
101
+ /* Displays error in console
102
+ * TODO:
103
+ * - get ignored codes from Plone error_log
104
+ */
105
+ const ignoredErrors = [301, 302, 401, 404];
106
+ if (!ignoredErrors.includes(err.status)) console.error(err);
107
+
108
+ res
109
+ .status(err.status || 500) // If error happens in Volto code itself error status is undefined
110
+ .send(`<!doctype html> ${renderToString(errorPage)}`);
111
+ }
112
+ });
113
+
114
+ function setupServer(req, res, next) {
115
+ const api = new Api(req);
116
+
117
+ const lang = toReactIntlLang(
118
+ new locale.Locales(
119
+ req.universalCookies.get('I18N_LANGUAGE') ||
120
+ config.settings.defaultLanguage ||
121
+ req.headers['accept-language'],
122
+ )
123
+ .best(supported)
124
+ .toString(),
125
+ );
126
+
127
+ // Minimum initial state for the fake Redux store instance
128
+ const initialState = {
129
+ intl: {
130
+ defaultLocale: 'en',
131
+ locale: lang,
132
+ messages: locales[lang],
133
+ },
134
+ };
135
+
136
+ const history = createMemoryHistory({
137
+ initialEntries: [req.url],
138
+ });
139
+
140
+ // Create a fake Redux store instance for the `errorHandler` to render
141
+ // and for being used by the rest of the middlewares, if required
142
+ const store = configureStore(initialState, history, api);
143
+
144
+ function errorHandler(error) {
145
+ const errorPage = (
146
+ <Provider store={store} onError={reactIntlErrorHandler}>
147
+ <StaticRouter context={{}} location={req.url}>
148
+ <ErrorPage message={error.message} />
149
+ </StaticRouter>
150
+ </Provider>
151
+ );
152
+
153
+ res.set({
154
+ 'Cache-Control': 'public, max-age=60, no-transform',
155
+ });
156
+
157
+ /* Displays error in console
158
+ * TODO:
159
+ * - get ignored codes from Plone error_log
160
+ */
161
+ const ignoredErrors = [301, 302, 401, 404];
162
+ if (!ignoredErrors.includes(error.status)) console.error(error);
163
+
164
+ res
165
+ .status(error.status || 500) // If error happens in Volto code itself error status is undefined
166
+ .send(`<!doctype html> ${renderToString(errorPage)}`);
167
+ }
168
+
169
+ if (!process.env.RAZZLE_API_PATH && req.headers.host) {
170
+ res.locals.detectedHost = `${
171
+ req.headers['x-forwarded-proto'] || req.protocol
172
+ }://${req.headers.host}`;
173
+ config.settings.apiPath = res.locals.detectedHost;
174
+ config.settings.publicURL = res.locals.detectedHost;
175
+ }
176
+
177
+ res.locals = {
178
+ ...res.locals,
179
+ store,
180
+ api,
181
+ errorHandler,
182
+ };
183
+
184
+ next();
185
+ }
186
+
187
+ server.get('/*', (req, res) => {
188
+ const { errorHandler } = res.locals;
189
+
190
+ const api = new Api(req);
191
+
192
+ const browserdetect = detect(req.headers['user-agent']);
193
+
194
+ const lang = toReactIntlLang(
195
+ new locale.Locales(
196
+ req.universalCookies.get('I18N_LANGUAGE') ||
197
+ config.settings.defaultLanguage ||
198
+ req.headers['accept-language'],
199
+ )
200
+ .best(supported)
201
+ .toString(),
202
+ );
203
+
204
+ const authToken = req.universalCookies.get('auth_token');
205
+ const initialState = {
206
+ userSession: { ...userSession(), token: authToken },
207
+ form: req.body,
208
+ intl: {
209
+ defaultLocale: 'en',
210
+ locale: lang,
211
+ messages: locales[lang],
212
+ },
213
+ browserdetect,
214
+ };
215
+
216
+ const history = createMemoryHistory({
217
+ initialEntries: [req.url],
218
+ });
219
+
220
+ // Create a new Redux store instance
221
+ const store = configureStore(initialState, history, api);
222
+
223
+ persistAuthToken(store, req);
224
+
225
+ // @loadable/server extractor
226
+ const buildDir = process.env.BUILD_DIR || 'build';
227
+ const extractor = new ChunkExtractor({
228
+ statsFile: path.resolve(path.join(buildDir, 'loadable-stats.json')),
229
+ entrypoints: ['client'],
230
+ });
231
+
232
+ const url = req.originalUrl || req.url;
233
+ const location = parseUrl(url);
234
+
235
+ loadOnServer({ store, location, routes, api })
236
+ .then(() => {
237
+ const initialLang =
238
+ req.universalCookies.get('I18N_LANGUAGE') ||
239
+ config.settings.defaultLanguage ||
240
+ req.headers['accept-language'];
241
+
242
+ // The content info is in the store at this point thanks to the asynconnect
243
+ // features, then we can force the current language info into the store when
244
+ // coming from an SSR request
245
+
246
+ // TODO: there is a bug here with content that, for any reason, doesn't
247
+ // present the language token field, for some reason. In this case, we
248
+ // should follow the cookie rather then switching the language
249
+ const contentLang = store.getState().content.get?.error
250
+ ? initialLang
251
+ : store.getState().content.data?.language?.token ||
252
+ config.settings.defaultLanguage;
253
+
254
+ if (toBackendLang(initialLang) !== contentLang) {
255
+ const newLang = toReactIntlLang(
256
+ new locale.Locales(contentLang).best(supported).toString(),
257
+ );
258
+ store.dispatch(changeLanguage(newLang, locales[newLang], req));
259
+ }
260
+
261
+ const context = {};
262
+ resetServerContext();
263
+ const markup = renderToString(
264
+ <ChunkExtractorManager extractor={extractor}>
265
+ <CookiesProvider cookies={req.universalCookies}>
266
+ <Provider store={store} onError={reactIntlErrorHandler}>
267
+ <StaticRouter context={context} location={req.url}>
268
+ <ReduxAsyncConnect routes={routes} helpers={api} />
269
+ </StaticRouter>
270
+ </Provider>
271
+ </CookiesProvider>
272
+ </ChunkExtractorManager>,
273
+ );
274
+
275
+ const readCriticalCss =
276
+ config.settings.serverConfig.readCriticalCss || defaultReadCriticalCss;
277
+
278
+ // If we are showing an "old browser" warning,
279
+ // make sure it doesn't get cached in a shared cache
280
+ const browserdetect = store.getState().browserdetect;
281
+ if (config.settings.notSupportedBrowsers.includes(browserdetect?.name)) {
282
+ res.set({
283
+ 'Cache-Control': 'private',
284
+ });
285
+ }
286
+
287
+ if (context.url) {
288
+ res.redirect(flattenToAppURL(context.url));
289
+ } else if (context.error_code) {
290
+ res.set({
291
+ 'Cache-Control': 'no-cache',
292
+ });
293
+
294
+ res.status(context.error_code).send(
295
+ `<!doctype html>
296
+ ${renderToString(
297
+ <Html
298
+ extractor={extractor}
299
+ markup={markup}
300
+ store={store}
301
+ extractScripts={
302
+ config.settings.serverConfig.extractScripts?.errorPages ||
303
+ process.env.NODE_ENV !== 'production'
304
+ }
305
+ criticalCss={readCriticalCss(req)}
306
+ apiPath={res.locals.detectedHost || config.settings.apiPath}
307
+ publicURL={
308
+ res.locals.detectedHost || config.settings.publicURL
309
+ }
310
+ />,
311
+ )}
312
+ `,
313
+ );
314
+ } else {
315
+ res.status(200).send(
316
+ `<!doctype html>
317
+ ${renderToString(
318
+ <Html
319
+ extractor={extractor}
320
+ markup={markup}
321
+ store={store}
322
+ criticalCss={readCriticalCss(req)}
323
+ apiPath={res.locals.detectedHost || config.settings.apiPath}
324
+ publicURL={
325
+ res.locals.detectedHost || config.settings.publicURL
326
+ }
327
+ />,
328
+ )}
329
+ `,
330
+ );
331
+ }
332
+ }, errorHandler)
333
+ .catch(errorHandler);
334
+ });
335
+
336
+ export const defaultReadCriticalCss = () => {
337
+ const { criticalCssPath } = config.settings.serverConfig;
338
+
339
+ const e = existsSync(criticalCssPath);
340
+ if (!e) return;
341
+
342
+ const f = lstatSync(criticalCssPath);
343
+ if (!f.isFile()) return;
344
+
345
+ return readFileSync(criticalCssPath, { encoding: 'utf-8' });
346
+ };
347
+
348
+ // Exposed for the console bootstrap info messages
349
+ server.apiPath = config.settings.apiPath;
350
+ server.devProxyToApiPath = config.settings.devProxyToApiPath;
351
+ server.proxyRewriteTarget = config.settings.proxyRewriteTarget;
352
+ server.publicURL = config.settings.publicURL;
353
+
354
+ export default server;
package/src/index.js CHANGED
@@ -72,16 +72,21 @@ const applyConfig = (config) => {
72
72
 
73
73
  config.settings.dateLocale = 'en-gb';
74
74
  config.settings.isMultilingual = true;
75
- config.settings.defaultLanguage =
76
- config.settings.eea?.defaultLanguage || 'en';
77
- // config.settings.supportedLanguages = config.settings.eea?.languages?.map(
78
- // (item) => item.code,
79
- // ) || ['en'];
80
- config.settings.supportedLanguages = ['en', 'de', 'fr', 'es', 'it'];
75
+ config.settings.hasLanguageDropdown = true;
76
+ config.settings.defaultLanguage = 'en';
77
+ config.settings.supportedLanguages = ['en', 'de', 'fr', 'es', 'it', 'pl'];
81
78
 
82
79
  // EEA customizations
83
80
  config.settings.eea = {
84
81
  ...(config.settings.eea || {}),
82
+ languages: [
83
+ { name: 'English', code: 'en' },
84
+ { name: 'Deutsch', code: 'de' },
85
+ { name: 'Français', code: 'fr' },
86
+ { name: 'Español', code: 'es' },
87
+ { name: 'Italiano', code: 'it' },
88
+ { name: 'Polski', code: 'pl' },
89
+ ],
85
90
  headerOpts: {
86
91
  ...(config.settings.eea?.headerOpts || {}),
87
92
  logo: ccaLogo,
@@ -9,6 +9,10 @@ body.subsite-mkh {
9
9
  }
10
10
  }
11
11
 
12
+ #language-switcher {
13
+ display: none;
14
+ }
15
+
12
16
  .subfooter .footer-description {
13
17
  margin-top: 3em;
14
18
 
@@ -24,6 +24,10 @@ p.has--clear--both:empty {
24
24
  margin-bottom: 0;
25
25
  }
26
26
 
27
+ .ui.basic.segment.content-area {
28
+ padding: 0;
29
+ }
30
+
27
31
  // Add icon for external links
28
32
  #page-document {
29
33
  p {