@eeacms/volto-cca-policy 0.3.41 → 0.3.43
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 +29 -0
- package/out.diff +55 -0
- package/package.json +2 -2
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.js +2 -1
- package/src/components/theme/MissionSignatoryProfile/AccordionList.test.jsx +0 -1
- package/src/components/theme/MissionSignatoryProfile/MissionSignatoryProfileView.jsx +35 -29
- package/src/components/theme/MissionSignatoryProfile/MissionSignatoryProfileView.test.jsx +8 -4
- package/src/components/theme/MissionSignatoryProfile/StatisticSection.jsx +5 -10
- package/src/components/theme/MissionSignatoryProfile/TabSections/ActionPagesTab.jsx +25 -10
- package/src/components/theme/MissionSignatoryProfile/TabSections/ActionPagesTab.test.jsx +1 -3
- package/src/components/theme/MissionSignatoryProfile/TabSections/AssessmentTab.test.jsx +1 -2
- package/src/components/theme/MissionSignatoryProfile/TabSections/GovernanceTab.test.jsx +1 -2
- package/src/components/theme/MissionSignatoryProfile/TabSections/PlanningTab.test.jsx +1 -2
- package/src/customizations/volto/components/theme/App/App.jsx +342 -0
- package/src/customizations/volto/components/theme/App/README.md +1 -0
- package/src/customizations/volto/components/theme/View/DefaultView.jsx +4 -3
- package/src/utils.js +4 -2
- package/theme/globals/mission.less +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ 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.3.43](https://github.com/eea/volto-cca-policy/compare/0.3.42...0.3.43) - 16 May 2025
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: update formatTextToHTML - refs #287671 [kreafox - [`79e5faf`](https://github.com/eea/volto-cca-policy/commit/79e5fafa7b2ba45405284b5f61865cb841c06e25)]
|
|
12
|
+
|
|
13
|
+
#### :nail_care: Enhancements
|
|
14
|
+
|
|
15
|
+
- change(mission): add fallback when action is empty - refs #286863 [kreafox - [`c99d5ea`](https://github.com/eea/volto-cca-policy/commit/c99d5ea1cf73796639585dca5fb2c524ac9bf91f)]
|
|
16
|
+
- change: update templates & tests [kreafox - [`57b9083`](https://github.com/eea/volto-cca-policy/commit/57b90839efb13377b47c53127596d88cb96b3246)]
|
|
17
|
+
- change(mission): add country subtitle - refs #287773 [kreafox - [`a8d07b7`](https://github.com/eea/volto-cca-policy/commit/a8d07b72cf71194bbbe5ea0559a3e8cf72ca221d)]
|
|
18
|
+
|
|
19
|
+
#### :house: Internal changes
|
|
20
|
+
|
|
21
|
+
- style(mission): adjust font size - refs #287671 [kreafox - [`50645b7`](https://github.com/eea/volto-cca-policy/commit/50645b75ae7d204a1cd47a78c3caa10bd3f63499)]
|
|
22
|
+
|
|
23
|
+
#### :hammer_and_wrench: Others
|
|
24
|
+
|
|
25
|
+
- No console.log [Tiberiu Ichim - [`6479dcc`](https://github.com/eea/volto-cca-policy/commit/6479dcc3a0f134d81fd4c3407692fa1d839a7093)]
|
|
26
|
+
### [0.3.42](https://github.com/eea/volto-cca-policy/compare/0.3.41...0.3.42) - 15 May 2025
|
|
27
|
+
|
|
28
|
+
#### :rocket: Dependency updates
|
|
29
|
+
|
|
30
|
+
- Release @eeacms/volto-globalsearch@2.1.2 [EEA Jenkins - [`e8281dd`](https://github.com/eea/volto-cca-policy/commit/e8281dd309f4c2734d137946554d1ff50d3e5c97)]
|
|
31
|
+
|
|
32
|
+
#### :bug: Bug Fixes
|
|
33
|
+
|
|
34
|
+
- fix: production crash by using safe Statistic subcomponent access [kreafox - [`af7b1d7`](https://github.com/eea/volto-cca-policy/commit/af7b1d745b0080e0a01325b35b860dc537510689)]
|
|
35
|
+
|
|
7
36
|
### [0.3.41](https://github.com/eea/volto-cca-policy/compare/0.3.40...0.3.41) - 14 May 2025
|
|
8
37
|
|
|
9
38
|
#### :rocket: Dependency updates
|
package/out.diff
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
diff --git a/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.js b/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.js
|
|
2
|
+
index f1f256d8..85d0490e 100644
|
|
3
|
+
--- a/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.js
|
|
4
|
+
+++ b/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.js
|
|
5
|
+
@@ -72,7 +72,8 @@ export default function CaseStudyExplorerView(props) {
|
|
6
|
+
setActiveItems(activeItems);
|
|
7
|
+
}, [activeFilters, cases]);
|
|
8
|
+
|
|
9
|
+
- if (__SERVER__) return '';
|
|
10
|
+
+ if (__SERVER__)
|
|
11
|
+
+ return <div className="casestudy-explorer-map">CaseStudyExplorer</div>;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="casestudy-explorer-map">
|
|
15
|
+
diff --git a/src/components/theme/BannerTitle/BannerTitle.jsx b/src/components/theme/BannerTitle/BannerTitle.jsx
|
|
16
|
+
index 386eb9e1..22f1b255 100644
|
|
17
|
+
--- a/src/components/theme/BannerTitle/BannerTitle.jsx
|
|
18
|
+
+++ b/src/components/theme/BannerTitle/BannerTitle.jsx
|
|
19
|
+
@@ -21,8 +21,10 @@ const BannerTitle = (props) => {
|
|
20
|
+
const hasHomePageClass = bodyClasses.includes('homepage');
|
|
21
|
+
setHasBodyClass(hasHomePageClass);
|
|
22
|
+
}, []);
|
|
23
|
+
+ const isChromeless =
|
|
24
|
+
+ __CLIENT__ && window?.location?.search?.indexOf('chromeless=1') > -1;
|
|
25
|
+
|
|
26
|
+
- return isHomePage ? null : (
|
|
27
|
+
+ return isHomePage || isChromeless ? null : (
|
|
28
|
+
<>
|
|
29
|
+
{!hasTitleBlock && !hasCountryFlagBlock ? (
|
|
30
|
+
<>
|
|
31
|
+
diff --git a/src/customizations/volto/components/theme/View/DefaultView.jsx b/src/customizations/volto/components/theme/View/DefaultView.jsx
|
|
32
|
+
index 90de6436..48ec77b6 100644
|
|
33
|
+
--- a/src/customizations/volto/components/theme/View/DefaultView.jsx
|
|
34
|
+
+++ b/src/customizations/volto/components/theme/View/DefaultView.jsx
|
|
35
|
+
@@ -85,7 +85,8 @@ const DefaultView = (props) => {
|
|
36
|
+
: 3;
|
|
37
|
+
const currentLang = useSelector((state) => state.intl.locale);
|
|
38
|
+
|
|
39
|
+
- // If the content is not yet loaded, then do not show anything
|
|
40
|
+
+ const isChromeless = location.search?.indexOf('chromeless=1') > -1;
|
|
41
|
+
+
|
|
42
|
+
return contentLoaded ? (
|
|
43
|
+
hasBlocksData(content) ? (
|
|
44
|
+
<>
|
|
45
|
+
@@ -123,8 +124,8 @@ const DefaultView = (props) => {
|
|
46
|
+
</Grid>
|
|
47
|
+
</Container>
|
|
48
|
+
) : (
|
|
49
|
+
- <Container id="page-document">
|
|
50
|
+
- <BannerTitle {...props} />
|
|
51
|
+
+ <Container id="page-document" className="here">
|
|
52
|
+
+ {!isChromeless && <BannerTitle {...props} />}
|
|
53
|
+
<RenderBlocks {...props} path={path} />
|
|
54
|
+
</Container>
|
|
55
|
+
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eeacms/volto-cca-policy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.43",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@eeacms/volto-eea-design-system": "<=1.36.3",
|
|
33
33
|
"@eeacms/volto-eea-website-theme": "^1.35.0",
|
|
34
34
|
"@eeacms/volto-embed": "^9.1.1",
|
|
35
|
-
"@eeacms/volto-globalsearch": "2.1.
|
|
35
|
+
"@eeacms/volto-globalsearch": "2.1.2",
|
|
36
36
|
"@eeacms/volto-hero-block": "^7.1.0",
|
|
37
37
|
"@eeacms/volto-openlayers-map": "*",
|
|
38
38
|
"@eeacms/volto-searchlib": "2.0.16",
|
|
@@ -72,7 +72,8 @@ export default function CaseStudyExplorerView(props) {
|
|
|
72
72
|
setActiveItems(activeItems);
|
|
73
73
|
}, [activeFilters, cases]);
|
|
74
74
|
|
|
75
|
-
if (__SERVER__)
|
|
75
|
+
if (__SERVER__)
|
|
76
|
+
return <div className="casestudy-explorer-map">CaseStudyExplorer</div>;
|
|
76
77
|
|
|
77
78
|
return (
|
|
78
79
|
<div className="casestudy-explorer-map">
|
|
@@ -1,47 +1,50 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Tab, Container, Divider } from 'semantic-ui-react';
|
|
3
|
-
import {
|
|
3
|
+
import { formatTextToHTML } from '@eeacms/volto-cca-policy/utils';
|
|
4
|
+
import { BannerTitle, HTMLField } from '@eeacms/volto-cca-policy/helpers';
|
|
5
|
+
|
|
4
6
|
import GovernanceTab from './TabSections/GovernanceTab';
|
|
5
7
|
import AssessmentTab from './TabSections/AssessmentTab';
|
|
6
8
|
import PlanningTab from './TabSections/PlanningTab';
|
|
7
9
|
import ActionPagesTab from './TabSections/ActionPagesTab';
|
|
8
10
|
|
|
9
11
|
const tabRenderers = {
|
|
10
|
-
Governance_Label: (
|
|
11
|
-
Assessment_Label: (
|
|
12
|
-
Planning_Label: (
|
|
13
|
-
Action_Label: (
|
|
12
|
+
Governance_Label: (props) => <GovernanceTab {...props} />,
|
|
13
|
+
Assessment_Label: (props) => <AssessmentTab {...props} />,
|
|
14
|
+
Planning_Label: (props) => <PlanningTab {...props} />,
|
|
15
|
+
Action_Label: (props) => <ActionPagesTab {...props} />,
|
|
14
16
|
};
|
|
15
17
|
|
|
16
|
-
const MissionSignatoryProfileView = (
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
props?.content?.['@components']?.missionsignatoryprofile || {};
|
|
18
|
+
const MissionSignatoryProfileView = ({ content }) => {
|
|
19
|
+
const signatoryData =
|
|
20
|
+
content?.['@components']?.missionsignatoryprofile?.result || {};
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const {
|
|
23
|
+
governance = [{}],
|
|
24
|
+
planning = {},
|
|
25
|
+
assessment = {},
|
|
26
|
+
action = {},
|
|
27
|
+
footer_text = {},
|
|
28
|
+
tab_labels = [],
|
|
29
|
+
general_text = [{}],
|
|
30
|
+
} = signatoryData;
|
|
28
31
|
|
|
29
32
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
30
33
|
|
|
34
|
+
const tabData = {
|
|
35
|
+
Governance_Label: { result: governance[0], general_text: general_text[0] },
|
|
36
|
+
Assessment_Label: { result: assessment, general_text: general_text[0] },
|
|
37
|
+
Planning_Label: { result: planning, general_text: general_text[0] },
|
|
38
|
+
Action_Label: { result: action, general_text: general_text[0] },
|
|
39
|
+
};
|
|
40
|
+
|
|
31
41
|
const panes = tab_labels
|
|
32
42
|
.filter(({ key }) => key !== 'Language')
|
|
33
43
|
.map(({ key, value }) => {
|
|
34
|
-
const
|
|
35
|
-
const dataMap = {
|
|
36
|
-
Governance_Label: governance,
|
|
37
|
-
Assessment_Label: assessment,
|
|
38
|
-
Planning_Label: planning,
|
|
39
|
-
Action_Label: action,
|
|
40
|
-
};
|
|
41
|
-
|
|
44
|
+
const Renderer = tabRenderers[key];
|
|
42
45
|
return {
|
|
43
46
|
menuItem: value,
|
|
44
|
-
render: () => (
|
|
47
|
+
render: () => (Renderer ? Renderer(tabData[key]) : null),
|
|
45
48
|
};
|
|
46
49
|
});
|
|
47
50
|
|
|
@@ -57,11 +60,12 @@ const MissionSignatoryProfileView = (props) => {
|
|
|
57
60
|
hidePublishingDate: true,
|
|
58
61
|
hideDownloadButton: false,
|
|
59
62
|
hideShareButton: false,
|
|
63
|
+
subtitle: general_text?.[0]?.Country_Or_Area,
|
|
60
64
|
}}
|
|
61
65
|
/>
|
|
66
|
+
|
|
62
67
|
<div className="signatory-profile">
|
|
63
68
|
<br />
|
|
64
|
-
|
|
65
69
|
<Tab
|
|
66
70
|
menu={{
|
|
67
71
|
fluid: true,
|
|
@@ -75,11 +79,13 @@ const MissionSignatoryProfileView = (props) => {
|
|
|
75
79
|
panes={panes}
|
|
76
80
|
/>
|
|
77
81
|
|
|
78
|
-
{footer_text
|
|
82
|
+
{footer_text?.Disclaimer && (
|
|
79
83
|
<div className="footer-text">
|
|
80
84
|
<Divider />
|
|
81
|
-
<strong>{footer_text
|
|
82
|
-
<
|
|
85
|
+
<strong>{footer_text?.Disclaimer_Title}</strong>
|
|
86
|
+
<HTMLField
|
|
87
|
+
value={{ data: formatTextToHTML(footer_text.Disclaimer) }}
|
|
88
|
+
/>
|
|
83
89
|
</div>
|
|
84
90
|
)}
|
|
85
91
|
</div>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import { render, fireEvent, screen } from '@testing-library/react';
|
|
3
2
|
import '@testing-library/jest-dom';
|
|
4
3
|
import MissionSignatoryProfileView from './MissionSignatoryProfileView';
|
|
5
4
|
|
|
6
|
-
// Mock the tab components with minimal placeholders
|
|
7
5
|
jest.mock('./TabSections/GovernanceTab', () => () => (
|
|
8
6
|
<div>Mocked Governance</div>
|
|
9
7
|
));
|
|
@@ -12,8 +10,12 @@ jest.mock('./TabSections/AssessmentTab', () => () => (
|
|
|
12
10
|
));
|
|
13
11
|
jest.mock('./TabSections/PlanningTab', () => () => <div>Mocked Planning</div>);
|
|
14
12
|
jest.mock('./TabSections/ActionPagesTab', () => () => <div>Mocked Action</div>);
|
|
13
|
+
|
|
15
14
|
jest.mock('@eeacms/volto-cca-policy/helpers', () => ({
|
|
16
15
|
BannerTitle: ({ children }) => <div>{children}</div>,
|
|
16
|
+
HTMLField: ({ value }) => (
|
|
17
|
+
<div dangerouslySetInnerHTML={{ __html: value?.data }} />
|
|
18
|
+
),
|
|
17
19
|
}));
|
|
18
20
|
|
|
19
21
|
describe('MissionSignatoryProfileView', () => {
|
|
@@ -34,7 +36,7 @@ describe('MissionSignatoryProfileView', () => {
|
|
|
34
36
|
{ key: 'Assessment_Label', value: 'Assessment' },
|
|
35
37
|
{ key: 'Planning_Label', value: 'Planning & Target' },
|
|
36
38
|
{ key: 'Action_Label', value: 'Action' },
|
|
37
|
-
{ key: 'Language', value: 'en' },
|
|
39
|
+
{ key: 'Language', value: 'en' },
|
|
38
40
|
],
|
|
39
41
|
},
|
|
40
42
|
},
|
|
@@ -70,6 +72,8 @@ describe('MissionSignatoryProfileView', () => {
|
|
|
70
72
|
it('renders footer disclaimer text if present', () => {
|
|
71
73
|
render(<MissionSignatoryProfileView content={content} />);
|
|
72
74
|
expect(screen.getByText('Disclaimer Title')).toBeInTheDocument();
|
|
73
|
-
expect(
|
|
75
|
+
expect(
|
|
76
|
+
screen.getByText((content) => content.includes('This is a disclaimer.')),
|
|
77
|
+
).toBeInTheDocument();
|
|
74
78
|
});
|
|
75
79
|
});
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Statistic,
|
|
3
|
-
StatisticValue,
|
|
4
|
-
StatisticLabel,
|
|
5
|
-
StatisticGroup,
|
|
6
|
-
} from 'semantic-ui-react';
|
|
1
|
+
import { Statistic } from 'semantic-ui-react';
|
|
7
2
|
|
|
8
3
|
const StatisticSection = ({ statistic }) => {
|
|
9
4
|
return (
|
|
10
|
-
<
|
|
5
|
+
<Statistic.Group widths="two" size="small">
|
|
11
6
|
{statistic.map((stat, index) => (
|
|
12
7
|
<Statistic key={index}>
|
|
13
|
-
<
|
|
14
|
-
<
|
|
8
|
+
<Statistic.Value>{stat.value}</Statistic.Value>
|
|
9
|
+
<Statistic.Label>{stat.label}</Statistic.Label>
|
|
15
10
|
</Statistic>
|
|
16
11
|
))}
|
|
17
|
-
</
|
|
12
|
+
</Statistic.Group>
|
|
18
13
|
);
|
|
19
14
|
};
|
|
20
15
|
|
|
@@ -51,21 +51,36 @@ const ActionsTabContent = ({ action }) => {
|
|
|
51
51
|
{action.Funding_Sources && (
|
|
52
52
|
<>
|
|
53
53
|
<br />
|
|
54
|
-
<
|
|
54
|
+
<div className="funding-sources">
|
|
55
55
|
<span>{action.Funding_Sources_Label} </span>
|
|
56
|
-
<strong>
|
|
57
|
-
|
|
56
|
+
<strong>
|
|
57
|
+
<HTMLField
|
|
58
|
+
value={{ data: formatTextToHTML(action.Funding_Sources) }}
|
|
59
|
+
/>
|
|
60
|
+
</strong>
|
|
61
|
+
</div>
|
|
58
62
|
</>
|
|
59
63
|
)}
|
|
60
64
|
</>
|
|
61
65
|
);
|
|
62
66
|
};
|
|
63
67
|
|
|
64
|
-
const ActionPagesTab = ({ result }) => {
|
|
65
|
-
const {
|
|
66
|
-
const
|
|
68
|
+
const ActionPagesTab = ({ result, general_text }) => {
|
|
69
|
+
const { action_text, actions } = result || {};
|
|
70
|
+
const { No_Data_Reported_Label } = general_text || {};
|
|
71
|
+
const { Title, Abstract, Abstract_Line } = action_text?.[0] || {};
|
|
72
|
+
const hasNoActions = !(actions?.length > 0);
|
|
73
|
+
const hasNoText = !(action_text?.length > 0);
|
|
74
|
+
|
|
75
|
+
const sortedActions = [...(actions || [])].sort((a, b) => a.Order - b.Order);
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
if (hasNoActions && hasNoText) {
|
|
78
|
+
return (
|
|
79
|
+
<Tab.Pane>
|
|
80
|
+
<h5>{No_Data_Reported_Label}</h5>
|
|
81
|
+
</Tab.Pane>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
69
84
|
|
|
70
85
|
return (
|
|
71
86
|
<Tab.Pane>
|
|
@@ -77,11 +92,11 @@ const ActionPagesTab = ({ result }) => {
|
|
|
77
92
|
</Callout>
|
|
78
93
|
)}
|
|
79
94
|
|
|
80
|
-
{sortedActions
|
|
95
|
+
{sortedActions?.map((action, index) => {
|
|
81
96
|
return (
|
|
82
97
|
<div key={index} className="section-wrapper">
|
|
83
98
|
<h5 className="section-title">
|
|
84
|
-
<span className="section-number">{action
|
|
99
|
+
<span className="section-number">{action?.Order}. </span>
|
|
85
100
|
<HTMLField value={{ data: formatTextToHTML(action?.Action) }} />
|
|
86
101
|
</h5>
|
|
87
102
|
|
|
@@ -89,7 +104,7 @@ const ActionPagesTab = ({ result }) => {
|
|
|
89
104
|
variation="secondary"
|
|
90
105
|
accordions={[
|
|
91
106
|
{
|
|
92
|
-
title: action?.More_Details_Label
|
|
107
|
+
title: action?.More_Details_Label,
|
|
93
108
|
content: <ActionsTabContent action={action} />,
|
|
94
109
|
},
|
|
95
110
|
]}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, within } from '@testing-library/react';
|
|
3
1
|
import '@testing-library/jest-dom';
|
|
2
|
+
import { render, within } from '@testing-library/react';
|
|
4
3
|
import ActionPagesTab from './ActionPagesTab';
|
|
5
4
|
|
|
6
|
-
// Mocking components used inside
|
|
7
5
|
jest.mock('@eeacms/volto-eea-design-system/ui', () => ({
|
|
8
6
|
Callout: ({ children }) => <div>{children}</div>,
|
|
9
7
|
}));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
3
1
|
import '@testing-library/jest-dom';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
4
3
|
import AssessmentTab from './AssessmentTab';
|
|
5
4
|
|
|
6
5
|
jest.mock('@eeacms/volto-eea-design-system/ui', () => ({
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
1
|
import '@testing-library/jest-dom';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
4
3
|
import PlanningTab from './PlanningTab';
|
|
5
4
|
|
|
6
5
|
jest.mock('@eeacms/volto-cca-policy/helpers', () => ({
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App container.
|
|
3
|
+
* @module components/theme/App/App
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { Component } from 'react';
|
|
7
|
+
import jwtDecode from 'jwt-decode';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
import { connect } from 'react-redux';
|
|
10
|
+
import { compose } from 'redux';
|
|
11
|
+
import { asyncConnect, Helmet } from '@plone/volto/helpers';
|
|
12
|
+
import { Segment } from 'semantic-ui-react';
|
|
13
|
+
import { renderRoutes } from 'react-router-config';
|
|
14
|
+
import { Slide, ToastContainer, toast } from 'react-toastify';
|
|
15
|
+
import split from 'lodash/split';
|
|
16
|
+
import join from 'lodash/join';
|
|
17
|
+
import trim from 'lodash/trim';
|
|
18
|
+
import cx from 'classnames';
|
|
19
|
+
import config from '@plone/volto/registry';
|
|
20
|
+
import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable';
|
|
21
|
+
import { visitBlocks } from '@plone/volto/helpers/Blocks/Blocks';
|
|
22
|
+
import { injectIntl } from 'react-intl';
|
|
23
|
+
|
|
24
|
+
import Error from '@plone/volto/error';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
Breadcrumbs,
|
|
28
|
+
Footer,
|
|
29
|
+
Header,
|
|
30
|
+
Icon,
|
|
31
|
+
OutdatedBrowser,
|
|
32
|
+
AppExtras,
|
|
33
|
+
SkipLinks,
|
|
34
|
+
} from '@plone/volto/components';
|
|
35
|
+
import {
|
|
36
|
+
BodyClass,
|
|
37
|
+
getBaseUrl,
|
|
38
|
+
getView,
|
|
39
|
+
hasApiExpander,
|
|
40
|
+
isCmsUi,
|
|
41
|
+
} from '@plone/volto/helpers';
|
|
42
|
+
import {
|
|
43
|
+
getBreadcrumbs,
|
|
44
|
+
getContent,
|
|
45
|
+
getNavigation,
|
|
46
|
+
getTypes,
|
|
47
|
+
getWorkflow,
|
|
48
|
+
} from '@plone/volto/actions';
|
|
49
|
+
|
|
50
|
+
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
51
|
+
import MultilingualRedirector from '@plone/volto/components/theme/MultilingualRedirector/MultilingualRedirector';
|
|
52
|
+
import WorkingCopyToastsFactory from '@plone/volto/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory';
|
|
53
|
+
import LockingToastsFactory from '@plone/volto/components/manage/LockingToastsFactory/LockingToastsFactory';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @export
|
|
57
|
+
* @class App
|
|
58
|
+
* @extends {Component}
|
|
59
|
+
*/
|
|
60
|
+
export class App extends Component {
|
|
61
|
+
/**
|
|
62
|
+
* Property types.
|
|
63
|
+
* @property {Object} propTypes Property types.
|
|
64
|
+
* @static
|
|
65
|
+
*/
|
|
66
|
+
static propTypes = {
|
|
67
|
+
pathname: PropTypes.string.isRequired,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
state = {
|
|
71
|
+
hasError: false,
|
|
72
|
+
error: null,
|
|
73
|
+
errorInfo: null,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
constructor(props) {
|
|
77
|
+
super(props);
|
|
78
|
+
this.mainRef = React.createRef();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @method componentWillReceiveProps
|
|
83
|
+
* @param {Object} nextProps Next properties
|
|
84
|
+
* @returns {undefined}
|
|
85
|
+
*/
|
|
86
|
+
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
87
|
+
if (nextProps.pathname !== this.props.pathname) {
|
|
88
|
+
if (this.state.hasError) {
|
|
89
|
+
this.setState({ hasError: false });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* ComponentDidCatch
|
|
96
|
+
* @method ComponentDidCatch
|
|
97
|
+
* @param {string} error The error
|
|
98
|
+
* @param {string} info The info
|
|
99
|
+
* @returns {undefined}
|
|
100
|
+
*/
|
|
101
|
+
componentDidCatch(error, info) {
|
|
102
|
+
this.setState({ hasError: true, error, errorInfo: info });
|
|
103
|
+
config.settings.errorHandlers.forEach((handler) => handler(error));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dispatchContentClick = (event) => {
|
|
107
|
+
if (event.target === event.currentTarget) {
|
|
108
|
+
const rect = this.mainRef.current.getBoundingClientRect();
|
|
109
|
+
if (event.clientY > rect.bottom) {
|
|
110
|
+
document.dispatchEvent(new Event('voltoClickBelowContent'));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render method.
|
|
117
|
+
* @method render
|
|
118
|
+
* @returns {string} Markup for the component.
|
|
119
|
+
*/
|
|
120
|
+
render() {
|
|
121
|
+
const { views } = config;
|
|
122
|
+
const path = getBaseUrl(this.props.pathname);
|
|
123
|
+
const action = getView(this.props.pathname);
|
|
124
|
+
const isCmsUI = isCmsUi(this.props.pathname);
|
|
125
|
+
const ConnectionRefusedView = views.errorViews.ECONNREFUSED;
|
|
126
|
+
|
|
127
|
+
const language =
|
|
128
|
+
this.props.content?.language?.token ?? this.props.intl?.locale;
|
|
129
|
+
const isChromeless =
|
|
130
|
+
this.props.isChromelessSSR ||
|
|
131
|
+
(__CLIENT__ && window?.location?.search?.indexOf('chromeless=1') > -1);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<PluggablesProvider>
|
|
135
|
+
{language && (
|
|
136
|
+
<Helmet>
|
|
137
|
+
<html lang={language} />
|
|
138
|
+
</Helmet>
|
|
139
|
+
)}
|
|
140
|
+
<BodyClass className={`view-${action}view`} />
|
|
141
|
+
|
|
142
|
+
{/* Body class depending on content type */}
|
|
143
|
+
{this.props.content && this.props.content['@type'] && (
|
|
144
|
+
<BodyClass
|
|
145
|
+
className={`contenttype-${this.props.content['@type']
|
|
146
|
+
.replace(' ', '-')
|
|
147
|
+
.toLowerCase()}`}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Body class depending on sections */}
|
|
152
|
+
<BodyClass
|
|
153
|
+
className={cx({
|
|
154
|
+
[trim(join(split(this.props.pathname, '/'), ' section-'))]:
|
|
155
|
+
this.props.pathname !== '/',
|
|
156
|
+
siteroot: this.props.pathname === '/',
|
|
157
|
+
'is-authenticated': !!this.props.token,
|
|
158
|
+
'is-anonymous': !this.props.token,
|
|
159
|
+
'cms-ui': isCmsUI,
|
|
160
|
+
'public-ui': !isCmsUI,
|
|
161
|
+
})}
|
|
162
|
+
/>
|
|
163
|
+
{!isChromeless && (
|
|
164
|
+
<>
|
|
165
|
+
<SkipLinks />
|
|
166
|
+
<Header pathname={path} />
|
|
167
|
+
<Breadcrumbs pathname={path} />
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
<MultilingualRedirector
|
|
171
|
+
pathname={this.props.pathname}
|
|
172
|
+
contentLanguage={this.props.content?.language?.token}
|
|
173
|
+
>
|
|
174
|
+
<Segment
|
|
175
|
+
basic
|
|
176
|
+
className="content-area"
|
|
177
|
+
onClick={this.dispatchContentClick}
|
|
178
|
+
>
|
|
179
|
+
<main ref={this.mainRef}>
|
|
180
|
+
<OutdatedBrowser />
|
|
181
|
+
{this.props.connectionRefused ? (
|
|
182
|
+
<ConnectionRefusedView />
|
|
183
|
+
) : this.state.hasError ? (
|
|
184
|
+
<Error
|
|
185
|
+
message={this.state.error.message}
|
|
186
|
+
stackTrace={this.state.errorInfo.componentStack}
|
|
187
|
+
/>
|
|
188
|
+
) : (
|
|
189
|
+
renderRoutes(this.props.route.routes, {
|
|
190
|
+
staticContext: this.props.staticContext,
|
|
191
|
+
})
|
|
192
|
+
)}
|
|
193
|
+
</main>
|
|
194
|
+
</Segment>
|
|
195
|
+
</MultilingualRedirector>
|
|
196
|
+
{!isChromeless && <Footer />}
|
|
197
|
+
<LockingToastsFactory
|
|
198
|
+
content={this.props.content}
|
|
199
|
+
user={this.props.userId}
|
|
200
|
+
/>
|
|
201
|
+
<WorkingCopyToastsFactory content={this.props.content} />
|
|
202
|
+
<ToastContainer
|
|
203
|
+
position={toast.POSITION.BOTTOM_CENTER}
|
|
204
|
+
hideProgressBar
|
|
205
|
+
transition={Slide}
|
|
206
|
+
autoClose={5000}
|
|
207
|
+
closeButton={
|
|
208
|
+
<Icon
|
|
209
|
+
className="toast-dismiss-action"
|
|
210
|
+
name={clearSVG}
|
|
211
|
+
size="18px"
|
|
212
|
+
/>
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
<AppExtras {...this.props} />
|
|
216
|
+
</PluggablesProvider>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const __test__ = connect(
|
|
222
|
+
(state, props) => ({
|
|
223
|
+
pathname: props.location.pathname,
|
|
224
|
+
token: state.userSession.token,
|
|
225
|
+
content: state.content.data,
|
|
226
|
+
apiError: state.apierror.error,
|
|
227
|
+
connectionRefused: state.apierror.connectionRefused,
|
|
228
|
+
}),
|
|
229
|
+
{},
|
|
230
|
+
)(App);
|
|
231
|
+
|
|
232
|
+
export const fetchContent = async ({ store, location }) => {
|
|
233
|
+
const content = await store.dispatch(
|
|
234
|
+
getContent(getBaseUrl(location.pathname)),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const promises = [];
|
|
238
|
+
const { blocksConfig } = config.blocks;
|
|
239
|
+
|
|
240
|
+
const visitor = ([id, data]) => {
|
|
241
|
+
const blockType = data['@type'];
|
|
242
|
+
const { getAsyncData } = blocksConfig[blockType];
|
|
243
|
+
if (getAsyncData) {
|
|
244
|
+
const p = getAsyncData({
|
|
245
|
+
store,
|
|
246
|
+
dispatch: store.dispatch,
|
|
247
|
+
path: location.pathname,
|
|
248
|
+
location,
|
|
249
|
+
id,
|
|
250
|
+
data,
|
|
251
|
+
blocksConfig,
|
|
252
|
+
content,
|
|
253
|
+
});
|
|
254
|
+
if (!p?.length) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
'You should return a list of promises from getAsyncData',
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
promises.push(...p);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
visitBlocks(content, visitor);
|
|
264
|
+
|
|
265
|
+
await Promise.allSettled(promises);
|
|
266
|
+
|
|
267
|
+
return content;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export function connectAppComponent(AppComponent) {
|
|
271
|
+
return compose(
|
|
272
|
+
asyncConnect([
|
|
273
|
+
{
|
|
274
|
+
key: 'breadcrumbs',
|
|
275
|
+
promise: ({ location, store: { dispatch } }) => {
|
|
276
|
+
// Do not trigger the breadcrumbs action if the expander is present
|
|
277
|
+
if (
|
|
278
|
+
__SERVER__ &&
|
|
279
|
+
!hasApiExpander('breadcrumbs', getBaseUrl(location.pathname))
|
|
280
|
+
) {
|
|
281
|
+
return dispatch(getBreadcrumbs(getBaseUrl(location.pathname)));
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
key: 'content',
|
|
287
|
+
promise: ({ location, store }) =>
|
|
288
|
+
__SERVER__ && fetchContent({ store, location }),
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
key: 'navigation',
|
|
292
|
+
promise: ({ location, store: { dispatch } }) => {
|
|
293
|
+
// Do not trigger the navigation action if the expander is present
|
|
294
|
+
if (
|
|
295
|
+
__SERVER__ &&
|
|
296
|
+
!hasApiExpander('navigation', getBaseUrl(location.pathname))
|
|
297
|
+
) {
|
|
298
|
+
return dispatch(
|
|
299
|
+
getNavigation(
|
|
300
|
+
getBaseUrl(location.pathname),
|
|
301
|
+
config.settings.navDepth,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
key: 'types',
|
|
309
|
+
promise: ({ location, store: { dispatch } }) => {
|
|
310
|
+
// Do not trigger the types action if the expander is present
|
|
311
|
+
if (
|
|
312
|
+
__SERVER__ &&
|
|
313
|
+
!hasApiExpander('types', getBaseUrl(location.pathname))
|
|
314
|
+
) {
|
|
315
|
+
return dispatch(getTypes(getBaseUrl(location.pathname)));
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
key: 'workflow',
|
|
321
|
+
promise: ({ location, store: { dispatch } }) =>
|
|
322
|
+
__SERVER__ && dispatch(getWorkflow(getBaseUrl(location.pathname))),
|
|
323
|
+
},
|
|
324
|
+
]),
|
|
325
|
+
injectIntl,
|
|
326
|
+
connect((state, props) => {
|
|
327
|
+
return {
|
|
328
|
+
isChromelessSSR: props.location.search?.indexOf('chromeless') > -1,
|
|
329
|
+
pathname: props.location.pathname,
|
|
330
|
+
token: state.userSession.token,
|
|
331
|
+
userId: state.userSession.token
|
|
332
|
+
? jwtDecode(state.userSession.token).sub
|
|
333
|
+
: '',
|
|
334
|
+
content: state.content.data,
|
|
335
|
+
apiError: state.apierror.error,
|
|
336
|
+
connectionRefused: state.apierror.connectionRefused,
|
|
337
|
+
};
|
|
338
|
+
}, null),
|
|
339
|
+
)(AppComponent);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default connectAppComponent(App);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
override to support the chromeless parameter
|
|
@@ -85,7 +85,8 @@ const DefaultView = (props) => {
|
|
|
85
85
|
: 3;
|
|
86
86
|
const currentLang = useSelector((state) => state.intl.locale);
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
const isChromeless = location.search?.indexOf('chromeless=1') > -1;
|
|
89
|
+
|
|
89
90
|
return contentLoaded ? (
|
|
90
91
|
hasBlocksData(content) ? (
|
|
91
92
|
<>
|
|
@@ -123,8 +124,8 @@ const DefaultView = (props) => {
|
|
|
123
124
|
</Grid>
|
|
124
125
|
</Container>
|
|
125
126
|
) : (
|
|
126
|
-
<Container id="page-document">
|
|
127
|
-
<BannerTitle {...props} />
|
|
127
|
+
<Container id="page-document" className="here">
|
|
128
|
+
{!isChromeless && <BannerTitle {...props} />}
|
|
128
129
|
<RenderBlocks {...props} path={path} />
|
|
129
130
|
</Container>
|
|
130
131
|
)}
|
package/src/utils.js
CHANGED
|
@@ -92,11 +92,13 @@ export const formatTextToHTML = (text) => {
|
|
|
92
92
|
|
|
93
93
|
// Convert URLs to clickable links
|
|
94
94
|
formattedText = formattedText.replace(
|
|
95
|
-
/((https?:\/\/[^\s<>"]+))/g,
|
|
95
|
+
/(?<!["'>])((https?:\/\/[^\s<>"]+))/g,
|
|
96
96
|
'<a href="$1" target="_blank" rel="noreferrer">$1</a>',
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
-
return
|
|
99
|
+
return formattedText.includes('<p>') || formattedText.includes('<a>')
|
|
100
|
+
? formattedText
|
|
101
|
+
: `<p>${formattedText}</p>`;
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
export const extractPlanNameAndURL = (text) => {
|
|
@@ -127,6 +127,11 @@ body.subsite-mkh {
|
|
|
127
127
|
background-color: #fff !important;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
.funding-sources {
|
|
131
|
+
display: flex;
|
|
132
|
+
gap: 0.5em;
|
|
133
|
+
}
|
|
134
|
+
|
|
130
135
|
.section-wrapper-info {
|
|
131
136
|
display: flex;
|
|
132
137
|
justify-content: space-between;
|
|
@@ -211,6 +216,7 @@ body.subsite-mkh {
|
|
|
211
216
|
|
|
212
217
|
.footer-text {
|
|
213
218
|
margin-top: 3em;
|
|
219
|
+
font-size: 15px;
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
}
|