@eeacms/volto-cca-policy 1.0.17 → 1.0.19

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.
Files changed (23) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +4 -3
  4. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +63 -0
  5. package/src/components/Search/NavigatorCatalogue/utils.js +0 -12
  6. package/src/components/Search/NavigatorCatalogue/utils.test.js +0 -4
  7. package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +5 -6
  8. package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.test.jsx +94 -0
  9. package/src/components/theme/CompareTools/CompareToolsPanel.jsx +2 -3
  10. package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +54 -0
  11. package/src/components/theme/CompareTools/CompareToolsView.jsx +152 -87
  12. package/src/components/theme/CompareTools/CompareToolsView.test.jsx +117 -10
  13. package/src/components/theme/CompareTools/utils.js +33 -1
  14. package/src/components/theme/CompareTools/utils.test.jsx +128 -17
  15. package/src/components/theme/ToolThumbnail/ToolThumbnail.jsx +40 -0
  16. package/src/components/theme/ToolThumbnail/ToolThumbnail.test.jsx +29 -0
  17. package/src/components/theme/ToolThumbnail/utils.js +49 -0
  18. package/src/components/theme/ToolThumbnail/utils.test.js +124 -0
  19. package/src/components/theme/Views/ExtendedToolView.jsx +2 -0
  20. package/src/components/theme/Views/ExtendedToolView.test.jsx +67 -0
  21. package/src/components/theme/Views/RelatedTools.jsx +2 -6
  22. package/theme/globals/navigator.less +51 -7
  23. package/theme/globals/views.less +14 -18
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
+ ### [1.0.19](https://github.com/eea/volto-cca-policy/compare/1.0.18...1.0.19) - 10 September 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: enhance comparison table with new output type and adaptation support cycle step features [kreafox - [`40e56ad`](https://github.com/eea/volto-cca-policy/commit/40e56adb144e283ce18bf36e87db5c36f286ab9c)]
12
+ - feat: enhance CompareToolsView with new comparison options and styling [kreafox - [`a912b50`](https://github.com/eea/volto-cca-policy/commit/a912b5006af55aefbd024b494cd9c810bfb22684)]
13
+
14
+ #### :nail_care: Enhancements
15
+
16
+ - change: comparison table export by removing unused fields and improve export functionality [kreafox - [`f9ee0f7`](https://github.com/eea/volto-cca-policy/commit/f9ee0f74d02153b9a233ef13264c026503534038)]
17
+ - change: update comparison options handling and enhance tests for adaptation support cycle steps [kreafox - [`fc2736b`](https://github.com/eea/volto-cca-policy/commit/fc2736b293e938b3e13367d96d3c8158c5435f49)]
18
+
19
+ #### :hammer_and_wrench: Others
20
+
21
+ - test: improve test readability and structure in Compare Tools utilities [kreafox - [`b6fc550`](https://github.com/eea/volto-cca-policy/commit/b6fc550db8f3ddbadf093c56596968caf94900ad)]
22
+ ### [1.0.18](https://github.com/eea/volto-cca-policy/compare/1.0.17...1.0.18) - 10 September 2026
23
+
24
+ #### :rocket: New Features
25
+
26
+ - feat: implement ToolThumbnail component and integrate it across various views [kreafox - [`edd3087`](https://github.com/eea/volto-cca-policy/commit/edd3087830076efa968ef42d1ec99efa42010cd5)]
27
+
28
+ #### :hammer_and_wrench: Others
29
+
30
+ - test: update image display style assertions in tests for NavigatorCatalogueCardItem and ToolThumbnail components [kreafox - [`24e1437`](https://github.com/eea/volto-cca-policy/commit/24e14375674dca5e73d8919eb4578d1c184db21a)]
7
31
  ### [1.0.17](https://github.com/eea/volto-cca-policy/compare/1.0.16...1.0.17) - 8 September 2026
8
32
 
9
33
  #### :rocket: New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
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",
@@ -3,6 +3,8 @@ import { Checkbox, Icon, Popup } from 'semantic-ui-react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
4
  import ExternalLink from '@eeacms/search/components/Result/ExternalLink';
5
5
  import ResultContext from '@eeacms/search/components/Result/ResultContext';
6
+ import ToolThumbnail from '../../theme/ToolThumbnail/ToolThumbnail';
7
+ import { getToolThumbnailUrl } from '../../theme/ToolThumbnail/utils';
6
8
  import {
7
9
  getCompareToolTitle,
8
10
  getCompareToolUid,
@@ -136,6 +138,7 @@ const NavigatorCatalogueCardItem = (props) => {
136
138
  uid: getCompareToolUid(result),
137
139
  title: getCompareToolTitle(result),
138
140
  href: result.href,
141
+ image: getToolThumbnailUrl(result),
139
142
  };
140
143
  const { isSelected, isLimitReached, setSelected } =
141
144
  useCompareTools(compareTool);
@@ -146,9 +149,7 @@ const NavigatorCatalogueCardItem = (props) => {
146
149
 
147
150
  return (
148
151
  <div className={`navigator-catalogue-item${isSelected ? ' selected' : ''}`}>
149
- <div className="navigator-tool-icon large" aria-hidden="true">
150
- <Icon className="ri-file-line" />
151
- </div>
152
+ <ToolThumbnail result={result} size="large" />
152
153
 
153
154
  <div className="catalogue-item-main">
154
155
  <div className="catalogue-item-top">
@@ -76,6 +76,7 @@ describe('NavigatorCatalogueCardItem', () => {
76
76
  cca_uid: { raw: 'tool-uid' },
77
77
  title: 'Climate planning tool',
78
78
  href: 'https://example.com/tool',
79
+ image: '/uploaded-tool-thumb.jpg',
79
80
  publication_date: { raw: '2026-07-24' },
80
81
  cca_adaptation_sectors: {
81
82
  raw: ['Agriculture', 'Water', 'Health', 'Energy'],
@@ -138,6 +139,7 @@ describe('NavigatorCatalogueCardItem', () => {
138
139
  uid: 'tool-uid',
139
140
  title: 'Climate planning tool',
140
141
  href: 'https://example.com/tool',
142
+ image: '/uploaded-tool-thumb.jpg',
141
143
  });
142
144
  });
143
145
 
@@ -170,4 +172,65 @@ describe('NavigatorCatalogueCardItem', () => {
170
172
  expect(screen.getByText('Water')).toBeInTheDocument();
171
173
  expect(screen.queryByText('License:')).not.toBeInTheDocument();
172
174
  });
175
+
176
+ describe('thumbnail rendering and fallback', () => {
177
+ it('renders thumbnail image and smoothly transitions from placeholder on load', () => {
178
+ const { container } = renderCard({
179
+ title: 'Tool with image',
180
+ href: '/tools/my-tool',
181
+ image: { scales: { thumb: { download: '/uploaded-thumb.jpg' } } },
182
+ });
183
+
184
+ const img = container.querySelector('.navigator-tool-icon img');
185
+ expect(img).toBeInTheDocument();
186
+ expect(img).toHaveAttribute('src', '/uploaded-thumb.jpg');
187
+ expect(img.parentElement).toHaveClass('navigator-tool-icon', 'large');
188
+ expect(img).toHaveStyle({ display: 'none' });
189
+ expect(
190
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
191
+ ).toBeInTheDocument();
192
+
193
+ // Fire load event on image
194
+ fireEvent.load(img);
195
+
196
+ expect(img.style.display).toBe('');
197
+ expect(
198
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
199
+ ).not.toBeInTheDocument();
200
+ });
201
+
202
+ it('falls back to placeholder icon when image fails to load', () => {
203
+ const { container } = renderCard({
204
+ title: 'Tool with broken image',
205
+ href: '/tools/broken-tool',
206
+ });
207
+
208
+ const img = container.querySelector('.navigator-tool-icon img');
209
+ expect(img).toBeInTheDocument();
210
+
211
+ // Fire error event on image (e.g. 404 from backend)
212
+ fireEvent.error(img);
213
+
214
+ expect(
215
+ container.querySelector('.navigator-tool-icon img'),
216
+ ).not.toBeInTheDocument();
217
+ expect(
218
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
219
+ ).toBeInTheDocument();
220
+ });
221
+
222
+ it('renders placeholder icon directly when item has no image or href', () => {
223
+ const { container } = renderCard({
224
+ title: 'Tool without image',
225
+ image: null,
226
+ });
227
+
228
+ expect(
229
+ container.querySelector('.navigator-tool-icon img'),
230
+ ).not.toBeInTheDocument();
231
+ expect(
232
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
233
+ ).toBeInTheDocument();
234
+ });
235
+ });
173
236
  });
@@ -124,12 +124,6 @@ export const exportComparisonTable = (tools, getToolField) => {
124
124
  formatFunctionalityScore(getToolField(tool, 'functionality')),
125
125
  ),
126
126
  ],
127
- [
128
- 'Spatial scale',
129
- ...tools.map((tool) =>
130
- arrayFieldToString(getToolField(tool, 'cca_geographical_scale')),
131
- ),
132
- ],
133
127
  [
134
128
  'Output type',
135
129
  ...tools.map((tool) =>
@@ -144,12 +138,6 @@ export const exportComparisonTable = (tools, getToolField) => {
144
138
  ),
145
139
  ),
146
140
  ],
147
- [
148
- 'Sector',
149
- ...tools.map((tool) =>
150
- arrayFieldToString(getToolField(tool, 'cca_adaptation_sectors')),
151
- ),
152
- ],
153
141
  ];
154
142
 
155
143
  downloadCsv('navigator-catalogue-comparison.csv', rows);
@@ -104,10 +104,8 @@ describe('Navigator Catalogue utilities', () => {
104
104
  values: {
105
105
  accessibility_and_usability: 'High',
106
106
  functionality: 5,
107
- cca_geographical_scale: ['Local'],
108
107
  cca_type_of_outputs: ['Maps'],
109
108
  cca_adaptation_support_cycle_step: ['Step 1'],
110
- cca_adaptation_sectors: ['Water'],
111
109
  },
112
110
  },
113
111
  ];
@@ -118,9 +116,7 @@ describe('Navigator Catalogue utilities', () => {
118
116
  expect(csv).toContain('"Criteria","Tool A"');
119
117
  expect(csv).toContain('"Usability","High"');
120
118
  expect(csv).toContain('"Functionality","5/6"');
121
- expect(csv).toContain('"Spatial scale","Local"');
122
119
  expect(csv).toContain('"Output type","Maps"');
123
120
  expect(csv).toContain('"Adaptation support cycle step","Step 1"');
124
- expect(csv).toContain('"Sector","Water"');
125
121
  });
126
122
  });
@@ -6,6 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
6
6
  import { Button, Checkbox, Icon, Loader, Message } from 'semantic-ui-react';
7
7
  import URLManager from '@elastic/search-ui/lib/cjs/URLManager';
8
8
  import { useSearchContext } from '@eeacms/search/lib/hocs';
9
+ import ToolThumbnail from '../../theme/ToolThumbnail/ToolThumbnail';
9
10
  import guideSteps from '../../../search/navigator_guide/guideSteps';
10
11
  import { navigatorGuideStepAtom } from '../../../state';
11
12
  import { mergeGuideOptions } from './utils';
@@ -350,12 +351,10 @@ const NavigatorGuideContentView = ({ appConfig }) => {
350
351
  className="navigator-guide-preview-result"
351
352
  key={result._original?._id || result.href}
352
353
  >
353
- <div
354
- className="navigator-tool-icon medium"
355
- aria-hidden="true"
356
- >
357
- <Icon className="ri-stack-line" />
358
- </div>
354
+ <ToolThumbnail
355
+ result={result}
356
+ fallbackIcon="ri-stack-line"
357
+ />
359
358
  <div className="navigator-guide-preview-result-content">
360
359
  <small
361
360
  className="navigator-tool-provider"
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { fireEvent, render } from '@testing-library/react';
4
+ import { IntlProvider } from 'react-intl';
5
+ import { useAtom } from 'jotai';
6
+ import { useSelector } from 'react-redux';
7
+ import { useHistory } from 'react-router-dom';
8
+ import { useSearchContext } from '@eeacms/search/lib/hocs';
9
+ import NavigatorGuideContentView from './NavigatorGuideContentView';
10
+ import useGuideFacetOptions from './useGuideFacetOptions';
11
+
12
+ jest.mock('jotai', () => ({
13
+ ...jest.requireActual('jotai'),
14
+ useAtom: jest.fn(),
15
+ }));
16
+ jest.mock('react-redux', () => ({ useSelector: jest.fn() }));
17
+ jest.mock('react-router-dom', () => ({ useHistory: jest.fn() }));
18
+ jest.mock('@eeacms/search/lib/hocs', () => ({ useSearchContext: jest.fn() }));
19
+ jest.mock('./useGuideFacetOptions', () => ({
20
+ __esModule: true,
21
+ default: jest.fn(),
22
+ }));
23
+
24
+ const renderGuide = (result) => {
25
+ useSearchContext.mockReturnValue({
26
+ filters: [{ field: 'cca_adaptation_sectors.keyword', values: ['Water'] }],
27
+ facets: {},
28
+ results: [result],
29
+ totalResults: 1,
30
+ isLoading: false,
31
+ });
32
+ return render(
33
+ <IntlProvider locale="en">
34
+ <NavigatorGuideContentView appConfig={{ previewResultsLimit: 3 }} />
35
+ </IntlProvider>,
36
+ );
37
+ };
38
+
39
+ describe('NavigatorGuideContentView thumbnails', () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ useAtom.mockReturnValue([0, jest.fn()]);
43
+ useSelector.mockReturnValue('en');
44
+ useHistory.mockReturnValue({ push: jest.fn() });
45
+ useGuideFacetOptions.mockReturnValue({});
46
+ });
47
+
48
+ it('shows the uploaded result image after loading, with a stack icon while loading', () => {
49
+ const { container } = renderGuide({
50
+ title: 'Guide tool',
51
+ href: '/tools/guide-tool',
52
+ image: { scales: { thumb: { download: '/uploaded-guide-thumb.jpg' } } },
53
+ });
54
+ const thumbnail = container.querySelector('.navigator-tool-icon');
55
+ const img = thumbnail.querySelector('img');
56
+
57
+ expect(thumbnail).toHaveClass('medium');
58
+ expect(thumbnail).toHaveAttribute('aria-hidden', 'true');
59
+ expect(img).toHaveAttribute('src', '/uploaded-guide-thumb.jpg');
60
+ expect(img).toHaveStyle({ display: 'none' });
61
+ expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
62
+
63
+ fireEvent.load(img);
64
+
65
+ expect(img).not.toHaveStyle({ display: 'none' });
66
+ expect(thumbnail.querySelector('.ri-stack-line')).not.toBeInTheDocument();
67
+ });
68
+
69
+ it('falls back to the stack icon when the image fails', () => {
70
+ const { container } = renderGuide({
71
+ title: 'Guide tool',
72
+ href: '/tools/guide-tool',
73
+ image: '/broken.jpg',
74
+ });
75
+ const thumbnail = container.querySelector('.navigator-tool-icon');
76
+
77
+ fireEvent.error(thumbnail.querySelector('img'));
78
+
79
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
80
+ expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
81
+ });
82
+
83
+ it.each([null, false])('shows the stack icon for image: %s', (image) => {
84
+ const { container } = renderGuide({
85
+ title: 'Guide tool',
86
+ href: '/tools/guide-tool',
87
+ image,
88
+ });
89
+ const thumbnail = container.querySelector('.navigator-tool-icon');
90
+
91
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
92
+ expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
93
+ });
94
+ });
@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
4
4
  import { useHistory } from 'react-router-dom';
5
5
  import { Icon, Button } from 'semantic-ui-react';
6
6
  import { defineMessages, useIntl } from 'react-intl';
7
+ import ToolThumbnail from '../ToolThumbnail/ToolThumbnail';
7
8
  import {
8
9
  MAX_COMPARE_TOOLS,
9
10
  compareToolsAtom,
@@ -89,9 +90,7 @@ export const CompareToolsPanel = () => {
89
90
  <div className="compare-panel-tools">
90
91
  {effectiveSelectedTools.map((tool) => (
91
92
  <div key={tool.uid} className="compare-panel-tool">
92
- <span className="navigator-tool-icon small" aria-hidden="true">
93
- <Icon className="ri-file-line" />
94
- </span>
93
+ <ToolThumbnail result={tool} size="small" />
95
94
  <span className="compare-panel-tool-title">{tool.title}</span>
96
95
  <Button
97
96
  className="compare-panel-tool-clear"
@@ -101,4 +101,58 @@ describe('CompareToolsPanel', () => {
101
101
  fireEvent.click(screen.getByText('Clear all'));
102
102
  expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
103
103
  });
104
+
105
+ it('shows the selected tool thumbnail after loading and preserves empty slot icons', () => {
106
+ const { container } = renderPanel([
107
+ {
108
+ uid: 'one',
109
+ title: 'Tool one',
110
+ href: '/tools/one',
111
+ image: '/uploaded-tool-thumb.jpg',
112
+ },
113
+ ]);
114
+ const thumbnail = container.querySelector(
115
+ '.compare-panel-tool:not(.placeholder) .navigator-tool-icon',
116
+ );
117
+ const img = thumbnail.querySelector('img');
118
+
119
+ expect(thumbnail).toHaveClass('small');
120
+ expect(img).toHaveAttribute('src', '/uploaded-tool-thumb.jpg');
121
+ expect(img).toHaveStyle({ display: 'none' });
122
+ expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
123
+ expect(
124
+ container.querySelectorAll('.placeholder .ri-add-line'),
125
+ ).toHaveLength(3);
126
+ expect(container.querySelector('.placeholder img')).not.toBeInTheDocument();
127
+
128
+ fireEvent.load(img);
129
+
130
+ expect(img).not.toHaveStyle({ display: 'none' });
131
+ expect(thumbnail.querySelector('.ri-file-line')).not.toBeInTheDocument();
132
+ });
133
+
134
+ it('uses the URL of an older saved selection and falls back on image error', () => {
135
+ const { container } = renderPanel([
136
+ { uid: 'one', title: 'Tool one', href: '/tools/one' },
137
+ ]);
138
+ const thumbnail = container.querySelector('.navigator-tool-icon');
139
+ const img = thumbnail.querySelector('img');
140
+
141
+ expect(img).toHaveAttribute('src', '/tools/one/@@images/image/thumb');
142
+
143
+ fireEvent.error(img);
144
+
145
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
146
+ expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
147
+ });
148
+
149
+ it.each([null, false])('shows the file icon for image: %s', (image) => {
150
+ const { container } = renderPanel([
151
+ { uid: 'one', title: 'Tool one', href: '/tools/one', image },
152
+ ]);
153
+ const thumbnail = container.querySelector('.navigator-tool-icon');
154
+
155
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
156
+ expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
157
+ });
104
158
  });