@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 +24 -8
- package/package.json +1 -1
- package/src/components/MigrationButtons.jsx +24 -11
- package/src/components/manage/Blocks/Listing/IndicatorCardsListingView.jsx +68 -0
- package/src/components/manage/Blocks/Listing/OrganisationCardsListingView.jsx +1 -1
- package/src/components/manage/Blocks/Listing/index.js +8 -0
- package/src/components/manage/Blocks/Listing/styles.less +19 -1
- package/src/components/theme/Header.jsx +4 -58
- package/src/components/theme/LanguageSwitch.jsx +66 -0
- package/src/customizations/@eeacms/volto-eea-design-system/ui/Header/Header.jsx +1 -1
- package/src/customizations/volto/middleware/README.md +3 -0
- package/src/customizations/volto/middleware/api.js +347 -0
- package/src/customizations/volto/server.jsx +354 -0
- package/src/index.js +11 -6
- package/theme/globals/mission.less +4 -0
- package/theme/globals/site.overrides +4 -0
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
|
@@ -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
|
|
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={
|
|
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={
|
|
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={
|
|
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 = (
|
|
67
|
-
const
|
|
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
|
+
}
|
|
@@ -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.
|
|
76
|
-
|
|
77
|
-
|
|
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,
|