@eeacms/volto-cca-policy 0.3.25 → 0.3.26

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,22 @@ 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.26](https://github.com/eea/volto-cca-policy/compare/0.3.25...0.3.26) - 15 April 2025
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: add extractFirstURL to parse first URL from text; update formatTextToHTML - refs #285361 [kreafox - [`a348454`](https://github.com/eea/volto-cca-policy/commit/a348454d52a06c4cbd668a7ffeaebed74d4a6788)]
12
+
13
+ #### :nail_care: Enhancements
14
+
15
+ - change: better link handler, update planning section, update tests - refs #285361 [kreafox - [`547fbb7`](https://github.com/eea/volto-cca-policy/commit/547fbb7ac308c274b94ff961b4abbae629adf93e)]
16
+ - change: update Governance section - refs #285296 [kreafox - [`0a9b924`](https://github.com/eea/volto-cca-policy/commit/0a9b92433239cf8d288885e561d6ecdd1beac6b5)]
17
+
18
+ #### :house: Internal changes
19
+
20
+ - style: Automated code fix [eea-jenkins - [`7396574`](https://github.com/eea/volto-cca-policy/commit/7396574df65205350cedb1e105927cb9d01ce3ae)]
21
+ - style: Automated code fix [eea-jenkins - [`2f6ca50`](https://github.com/eea/volto-cca-policy/commit/2f6ca50ad766066511659ac8dd85a3daceeca1d8)]
22
+
7
23
  ### [0.3.25](https://github.com/eea/volto-cca-policy/compare/0.3.24...0.3.25) - 14 April 2025
8
24
 
9
25
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
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",
@@ -4,85 +4,29 @@ import { Callout } from '@eeacms/volto-eea-design-system/ui';
4
4
  import { HTMLField } from '@eeacms/volto-cca-policy/helpers';
5
5
  import { formatTextToHTML } from '@eeacms/volto-cca-policy/utils';
6
6
  import AccordionList from './../AccordionList';
7
- import StatisticsSection from './../StatisticsSection';
8
7
 
9
8
  const GovernanceTab = ({ result }) => {
10
- const statisticsData = [
11
- {
12
- value: '460km',
13
- label: 'Duis non quam et nisi tincidunt',
14
- },
15
- {
16
- value: '51-60%',
17
- label: 'Vestibulum ante ipsum primis',
18
- },
19
- {
20
- value: '2.431.213',
21
- label: 'Aliquam erat volutpat',
22
- },
23
- {
24
- value: '2023',
25
- label: 'Etiam accumsan urna a mauris',
26
- },
27
- ];
9
+ const { Introduction, Describe_Title, Describe, Provide_Title, Provide } =
10
+ result || {};
28
11
 
29
12
  return (
30
13
  <Tab.Pane>
31
14
  <h2>Governance</h2>
32
15
  <Callout>
33
- <p>
34
- Sed at risus vel nulla consequat fermentum. Donec et orci mauris.
35
- Nullam tempor velit id mi luctus, a scelerisque libero accumsan. In
36
- hac habitasse platea dictumst. Cras ac nunc nec massa tristique
37
- fringilla.
38
- </p>
16
+ <p>{Introduction}</p>
39
17
  </Callout>
40
18
 
41
- <StatisticsSection statistics={statisticsData} />
19
+ <h3>{Describe_Title}</h3>
42
20
 
43
- <h3>Climate related issues</h3>
44
- <AccordionList
45
- accordions={[
46
- {
47
- title: 'Vestibulum ante ipsum primis',
48
- content: 'No additional details provided.',
49
- },
50
- {
51
- title: 'Etiam accumsan urna a mauris',
52
- content: 'No additional details provided.',
53
- },
54
- ]}
55
- />
21
+ <HTMLField value={{ data: formatTextToHTML(Describe) }} />
56
22
 
57
- <h3>Opportunities and benefits of climate action</h3>
58
-
59
- <HTMLField value={{ data: formatTextToHTML(result?.Describe) }} />
23
+ <br />
60
24
 
61
25
  <AccordionList
62
26
  accordions={[
63
27
  {
64
- title: ' Further details and evidence',
65
- content: (
66
- <HTMLField value={{ data: formatTextToHTML(result?.Provide) }} />
67
- ),
68
- },
69
- ]}
70
- />
71
-
72
- <h3>
73
- {result?.Signatory} engages with other levels of government regarding
74
- their:
75
- </h3>
76
-
77
- <AccordionList
78
- accordions={[
79
- {
80
- title: 'Vestibulum ante ipsum primis',
81
- content: 'No additional details provided.',
82
- },
83
- {
84
- title: 'Etiam accumsan urna a mauris',
85
- content: 'No additional details provided.',
28
+ title: <>{Provide_Title}</>,
29
+ content: <HTMLField value={{ data: formatTextToHTML(Provide) }} />,
86
30
  },
87
31
  ]}
88
32
  />
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import GovernanceTab from './GovernanceTab';
5
+
6
+ describe('GovernanceTab', () => {
7
+ const mockResult = {
8
+ Describe_Title: 'Opportunities and benefits of climate action',
9
+ Provide_Title: 'Further details and evidence',
10
+ };
11
+
12
+ it('renders the governance tab correctly', () => {
13
+ const { getByText } = render(<GovernanceTab result={mockResult} />);
14
+
15
+ expect(
16
+ getByText('Opportunities and benefits of climate action'),
17
+ ).toBeInTheDocument();
18
+ expect(getByText('Further details and evidence')).toBeInTheDocument();
19
+ });
20
+ });
@@ -11,7 +11,10 @@ import {
11
11
  } from 'semantic-ui-react';
12
12
  import { Callout } from '@eeacms/volto-eea-design-system/ui';
13
13
  import { HTMLField } from '@eeacms/volto-cca-policy/helpers';
14
- import { formatTextToHTML } from '@eeacms/volto-cca-policy/utils';
14
+ import {
15
+ formatTextToHTML,
16
+ extractPlanNameAndURL,
17
+ } from '@eeacms/volto-cca-policy/utils';
15
18
  import AccordionList from './../AccordionList';
16
19
  import image from '@eeacms/volto-cca-policy/../theme/assets/images/image-narrow.svg';
17
20
 
@@ -51,7 +54,7 @@ const PlanningGoalContent = ({ goal }) => {
51
54
  <Grid.Column mobile={12} tablet={12} computer={hasHazards ? 6 : 12}>
52
55
  {hasComments && (
53
56
  <>
54
- <h5>{goal.Comments_Label}</h5>
57
+ <h5 className="small-label">{goal.Comments_Label}</h5>
55
58
  <Segment>
56
59
  <HTMLField value={{ data: formatTextToHTML(goal.Comments) }} />
57
60
  </Segment>
@@ -59,7 +62,7 @@ const PlanningGoalContent = ({ goal }) => {
59
62
  )}
60
63
  {hasDescription && (
61
64
  <>
62
- <h5>{goal.Description_Label}</h5>
65
+ <h5 className="small-label">{goal.Description_Label}</h5>
63
66
  <Segment>
64
67
  <HTMLField
65
68
  value={{ data: formatTextToHTML(goal.Description) }}
@@ -99,21 +102,17 @@ const PlanningTab = ({ result }) => {
99
102
  )}
100
103
 
101
104
  {sortedGoals.map((goal, index) => {
102
- const goalNumber = parseInt(
103
- goal.Adaptation_Goal_Id.replace(/\D/g, ''),
104
- 10,
105
- );
106
105
  return (
107
106
  <div key={index} className="section-wrapper">
108
- <h5>
109
- <span className="section-number">{goalNumber}. </span>
110
- {goal.Title}
111
- </h5>
107
+ <span className="goal-title-label">{goal?.Title_Label}</span>
108
+
109
+ <HTMLField value={{ data: formatTextToHTML(goal?.Title) }} />
110
+
112
111
  <AccordionList
113
- variation="secondary"
112
+ variation="tertiary"
114
113
  accordions={[
115
114
  {
116
- title: goal.More_Details_Label || 'More details',
115
+ title: goal?.More_Details_Label || 'More details',
117
116
  content: <PlanningGoalContent goal={goal} />,
118
117
  },
119
118
  ]}
@@ -123,19 +122,18 @@ const PlanningTab = ({ result }) => {
123
122
  })}
124
123
 
125
124
  {goalData?.Climate_Action_Title && (
126
- <>
127
- <h2>{goalData.Climate_Action_Title}</h2>
128
- {goalData?.Climate_Action_Abstract && (
129
- <Callout>
130
- <p>{goalData.Climate_Action_Abstract}</p>
131
- </Callout>
132
- )}
133
- </>
125
+ <h2>{goalData.Climate_Action_Title}</h2>
126
+ )}
127
+
128
+ {goalData?.Climate_Action_Abstract && (
129
+ <Callout>
130
+ <p>{goalData.Climate_Action_Abstract}</p>
131
+ </Callout>
134
132
  )}
135
133
 
136
134
  {planning_climate_action.map((action, index) => {
137
135
  return (
138
- <>
136
+ <React.Fragment key={index}>
139
137
  <br />
140
138
  {action?.Sectors_Introduction && (
141
139
  <Message>
@@ -143,7 +141,7 @@ const PlanningTab = ({ result }) => {
143
141
  </Message>
144
142
  )}
145
143
 
146
- <ItemsSection items={action.Sectors} />
144
+ <ItemsSection items={action?.Sectors} />
147
145
  {action?.Description && <p>{action.Description}</p>}
148
146
 
149
147
  {(action?.Approval_Year || action?.End_Year) && (
@@ -158,21 +156,26 @@ const PlanningTab = ({ result }) => {
158
156
  {action?.Name_Of_Plan_And_Hyperlink && (
159
157
  <p>
160
158
  {(() => {
161
- const [
162
- planName,
163
- planUrl,
164
- ] = action.Name_Of_Plan_And_Hyperlink.split(';').map((part) =>
165
- part.trim(),
159
+ const { name, url } = extractPlanNameAndURL(
160
+ action.Name_Of_Plan_And_Hyperlink,
166
161
  );
167
- return (
168
- <a href={planUrl} title={planName}>
169
- <strong>{action.Further_Information_Link_Text}</strong>
162
+
163
+ return url ? (
164
+ <a href={url} title={name} target="_blank" rel="noreferrer">
165
+ <strong>
166
+ {action.Further_Information_Link_Text}
167
+ {name && ` [${name}]`}
168
+ </strong>
170
169
  </a>
170
+ ) : (
171
+ <strong>
172
+ {action.Further_Information_Link_Text}
173
+ {name && ` [${name}]`}
174
+ </strong>
171
175
  );
172
176
  })()}
173
177
  </p>
174
178
  )}
175
-
176
179
  {action?.Attachment && (
177
180
  <p>
178
181
  <a href={action.Attachment}>
@@ -180,7 +183,7 @@ const PlanningTab = ({ result }) => {
180
183
  </a>
181
184
  </p>
182
185
  )}
183
- </>
186
+ </React.Fragment>
184
187
  );
185
188
  })}
186
189
  </Tab.Pane>
@@ -3,15 +3,18 @@ import { render } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import PlanningTab from './PlanningTab';
5
5
 
6
- // Mock child components
7
- jest.mock('./../AccordionList', () => () => <div>Mock AccordionList</div>);
8
6
  jest.mock('@eeacms/volto-cca-policy/helpers', () => ({
9
7
  HTMLField: ({ value }) => (
10
8
  <div dangerouslySetInnerHTML={{ __html: value.data }} />
11
9
  ),
12
10
  }));
11
+
13
12
  jest.mock('@eeacms/volto-cca-policy/utils', () => ({
14
13
  formatTextToHTML: (text) => text,
14
+ extractPlanNameAndURL: (str) => ({
15
+ name: 'Plan Example',
16
+ url: 'https://plan-link.com',
17
+ }),
15
18
  }));
16
19
 
17
20
  describe('PlanningTab', () => {
@@ -23,6 +26,7 @@ describe('PlanningTab', () => {
23
26
  {
24
27
  Adaptation_Goal_Id: 'AG-001',
25
28
  Title: 'Goal Title 1',
29
+ Title_Label: 'Goal 1 Label',
26
30
  More_Details_Label: 'Details',
27
31
  Climate_Hazards: ['Heat', 'Flood'],
28
32
  Climate_Hazards_Addressed_Label: 'Hazards',
@@ -36,6 +40,7 @@ describe('PlanningTab', () => {
36
40
  {
37
41
  Adaptation_Goal_Id: 'AG-002',
38
42
  Title: 'Goal Title 2',
43
+ Title_Label: 'Goal 2 Label',
39
44
  More_Details_Label: 'Details',
40
45
  Climate_Hazards: ['Drought', 'Storm'],
41
46
  Climate_Hazards_Addressed_Label: 'Hazards',
@@ -65,9 +70,7 @@ describe('PlanningTab', () => {
65
70
  };
66
71
 
67
72
  it('renders planning tab with basic data', () => {
68
- const { getByText, getAllByText } = render(
69
- <PlanningTab result={mockResult} />,
70
- );
73
+ const { getByText } = render(<PlanningTab result={mockResult} />);
71
74
 
72
75
  expect(getByText('Planning Title')).toBeInTheDocument();
73
76
  expect(getByText('Abstract info')).toBeInTheDocument();
@@ -75,8 +78,6 @@ describe('PlanningTab', () => {
75
78
  expect(getByText('Goal Title 1')).toBeInTheDocument();
76
79
  expect(getByText('Goal Title 2')).toBeInTheDocument();
77
80
 
78
- expect(getAllByText('Mock AccordionList').length).toBe(2);
79
-
80
81
  expect(getByText('Action Title')).toBeInTheDocument();
81
82
  expect(getByText('Intro to sectors')).toBeInTheDocument();
82
83
  expect(getByText('Sector description')).toBeInTheDocument();
@@ -85,8 +86,6 @@ describe('PlanningTab', () => {
85
86
  expect(getByText(/2023/)).toBeInTheDocument();
86
87
  expect(getByText(/End Year:/)).toBeInTheDocument();
87
88
  expect(getByText(/2030/)).toBeInTheDocument();
88
-
89
- expect(getByText('Explore Plan')).toBeInTheDocument();
90
89
  });
91
90
 
92
91
  it('renders ItemsSection if there are sectors', () => {
@@ -94,4 +93,18 @@ describe('PlanningTab', () => {
94
93
 
95
94
  expect(getByText(/Agriculture/)).toBeInTheDocument();
96
95
  });
96
+
97
+ it('renders image in ItemsSection', () => {
98
+ const { container } = render(<PlanningTab result={mockResult} />);
99
+ const images = container.querySelectorAll('img');
100
+ expect(images.length).toBeGreaterThan(0);
101
+ });
102
+
103
+ it('renders hyperlink with extracted name and URL', () => {
104
+ const { getByText } = render(<PlanningTab result={mockResult} />);
105
+
106
+ const link = getByText(/More Info \[Plan Example\]/);
107
+ expect(link).toBeInTheDocument();
108
+ expect(link.closest('a')).toHaveAttribute('href', 'https://plan-link.com');
109
+ });
97
110
  });
@@ -24,12 +24,24 @@
24
24
  }
25
25
 
26
26
  .section-wrapper {
27
- padding: 1.5em;
28
- margin: 2em 0;
29
- background-color: #f9f9f9;
27
+ margin: 1em 0;
28
+
29
+ .goal-title-label {
30
+ display: inline-block;
31
+ padding: 0.3em 0.5em;
32
+ margin: 1em 0;
33
+ background-color: #dbe7f4;
34
+ font-size: 14px;
35
+ font-weight: bold;
36
+ text-transform: uppercase;
37
+ }
38
+
39
+ .ui.accordion {
40
+ margin-top: 1em;
41
+ }
30
42
 
31
- .section-number {
32
- color: @pineGreen;
43
+ .small-label {
44
+ font-size: 1em;
33
45
  }
34
46
  }
35
47
 
package/src/utils.js CHANGED
@@ -81,18 +81,41 @@ export const filterBlocks = (content, blockTypes = []) => {
81
81
  export const formatTextToHTML = (text) => {
82
82
  if (!text) return '';
83
83
 
84
- let formattedText = text;
84
+ let formattedText = text
85
+ .replace(/\\\\/g, '\\') // unescape backslashes
86
+ .replace(/\\'/g, "'") // unescape single quotes
87
+ .replace(/\\"/g, '"') // unescape double quotes
88
+ .replace(/\\t\\n/g, '') // handle \t\n
89
+ .replace(/\\n\\n/g, '</p><p>') // double line break = paragraph
90
+ .replace(/\\no\s*/g, '<br />• ') // list-like "o " to bullet point
91
+ .replace(/\\n/g, '<br />'); // single line break
85
92
 
86
- // Handle common escape issues
87
- formattedText = formattedText.replace(/\\\\/g, '\\'); // unescape backslashes
88
- formattedText = formattedText.replace(/\\'/g, "'"); // unescape single quotes
89
- formattedText = formattedText.replace(/\\"/g, '"'); // unescape double quotes
90
-
91
- // Replace \\n\\n with </p><p> for paragraph separation
92
- formattedText = formattedText.replace(/\\n\\n/g, '</p><p>');
93
+ return `<p>${formattedText}</p>`;
94
+ };
93
95
 
94
- // Replace \\n with <br /> for line breaks
95
- formattedText = formattedText.replace(/\\n/g, '<br />');
96
+ export const extractPlanNameAndURL = (text) => {
97
+ if (!text) return { name: '', url: '' };
98
+
99
+ // Match URL inside parentheses
100
+ const parenthesisMatch = text.match(/\((https?:\/\/[^\s)]+)\)/);
101
+ // Match first direct URL not inside parentheses
102
+ const directMatch = text.match(/https?:\/\/[^\s,;)]+/);
103
+ const url = parenthesisMatch?.[1] || directMatch?.[0] || '';
104
+
105
+ let name = text;
106
+
107
+ if (url) {
108
+ // Remove URL and any punctuation before it
109
+ name = name
110
+ .replace(`(${url})`, '')
111
+ .replace(url, '')
112
+ .replace(/[-–;,:\s]+$/, '')
113
+ .replace(/[-–;,:\s]+$/, '')
114
+ .trim();
115
+ }
96
116
 
97
- return `<p>${formattedText}</p>`;
117
+ return {
118
+ name: name,
119
+ url,
120
+ };
98
121
  };