@eeacms/volto-cca-policy 1.0.16 → 1.0.18
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 +23 -0
- package/package.json +1 -1
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +4 -3
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +63 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +1 -1
- package/src/components/Search/NavigatorCatalogue/utils.js +4 -7
- package/src/components/Search/NavigatorCatalogue/utils.test.js +11 -22
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +7 -9
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.test.jsx +94 -0
- package/src/components/theme/CompareTools/CompareToolsPanel.jsx +15 -13
- package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +74 -12
- package/src/components/theme/CompareTools/CompareToolsView.jsx +26 -31
- package/src/components/theme/CompareTools/CompareToolsView.test.jsx +47 -12
- package/src/components/theme/CompareTools/utils.js +19 -9
- package/src/components/theme/CompareTools/utils.test.jsx +21 -8
- package/src/components/theme/ToolThumbnail/ToolThumbnail.jsx +40 -0
- package/src/components/theme/ToolThumbnail/ToolThumbnail.test.jsx +29 -0
- package/src/components/theme/ToolThumbnail/utils.js +49 -0
- package/src/components/theme/ToolThumbnail/utils.test.js +124 -0
- package/src/components/theme/Views/ExtendedToolView.jsx +22 -0
- package/src/components/theme/Views/ExtendedToolView.test.jsx +93 -0
- package/src/components/theme/Views/RelatedTools.jsx +2 -6
- package/src/hocs/withClientOnly.jsx +13 -1
- package/src/hocs/withClientOnly.test.jsx +19 -0
- package/src/search/navigator_guide/config.js +0 -2
- package/theme/globals/navigator.less +9 -0
- package/theme/globals/views.less +24 -49
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.18](https://github.com/eea/volto-cca-policy/compare/1.0.17...1.0.18) - 10 September 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- feat: implement ToolThumbnail component and integrate it across various views [kreafox - [`edd3087`](https://github.com/eea/volto-cca-policy/commit/edd3087830076efa968ef42d1ec99efa42010cd5)]
|
|
12
|
+
|
|
13
|
+
#### :hammer_and_wrench: Others
|
|
14
|
+
|
|
15
|
+
- test: update image display style assertions in tests for NavigatorCatalogueCardItem and ToolThumbnail components [kreafox - [`24e1437`](https://github.com/eea/volto-cca-policy/commit/24e14375674dca5e73d8919eb4578d1c184db21a)]
|
|
16
|
+
### [1.0.17](https://github.com/eea/volto-cca-policy/compare/1.0.16...1.0.17) - 8 September 2026
|
|
17
|
+
|
|
18
|
+
#### :rocket: New Features
|
|
19
|
+
|
|
20
|
+
- feat(navigator): add back to search link on tool view; code refactoring & cleanup - refs #304608 [kreafox - [`b80edc4`](https://github.com/eea/volto-cca-policy/commit/b80edc476f073a4023a06759765cb1ed1da874eb)]
|
|
21
|
+
- feat: add MetadataTags component to display tool output types - refs #304612 [kreafox - [`da36a73`](https://github.com/eea/volto-cca-policy/commit/da36a7340da0d36a807b619e1da57d246b11b07d)]
|
|
22
|
+
|
|
23
|
+
#### :nail_care: Enhancements
|
|
24
|
+
|
|
25
|
+
- change: remove sectors from CompareToolsView - refs #304612 [kreafox - [`9b9352f`](https://github.com/eea/volto-cca-policy/commit/9b9352fa116a951ceef64511bc7c75779a7f0ec4)]
|
|
26
|
+
|
|
27
|
+
#### :hammer_and_wrench: Others
|
|
28
|
+
|
|
29
|
+
- test: remove sectors from CompareToolsView accessibility tests [kreafox - [`b3df4e2`](https://github.com/eea/volto-cca-policy/commit/b3df4e21aa79c6d9c726ad498fee2ce9af7f17a0)]
|
|
7
30
|
### [1.0.16](https://github.com/eea/volto-cca-policy/compare/1.0.15...1.0.16) - 4 September 2026
|
|
8
31
|
|
|
9
32
|
### [1.0.15](https://github.com/eea/volto-cca-policy/compare/1.0.14...1.0.15) - 4 September 2026
|
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
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
|
});
|
|
@@ -158,7 +158,7 @@ const NavigatorCatalogueContentView = (props) => {
|
|
|
158
158
|
{children.length === 0 && !isLoading && wasSearched && <NoResults />}
|
|
159
159
|
|
|
160
160
|
<ResultViewComponent>{children}</ResultViewComponent>
|
|
161
|
-
<CompareToolsPanel
|
|
161
|
+
<CompareToolsPanel />
|
|
162
162
|
|
|
163
163
|
{children.length > 0 && activeNavigatorViewId !== 'map' && (
|
|
164
164
|
<div className="search-body-footer">
|
|
@@ -97,19 +97,16 @@ export const downloadCsv = (filename, rows) => {
|
|
|
97
97
|
URL.revokeObjectURL(url);
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
export const
|
|
100
|
+
export const getNavigatorCataloguePageURL = (currentLang = 'en') => {
|
|
101
101
|
const lang = currentLang || 'en';
|
|
102
|
-
const landingPageURL =
|
|
103
|
-
appConfig?.landingPageURL || '/en/navigator/tool-catalogue';
|
|
104
102
|
|
|
105
|
-
return
|
|
103
|
+
return `/${lang}/navigator/tool-catalogue`;
|
|
106
104
|
};
|
|
107
105
|
|
|
108
|
-
export const getComparePageURL = (
|
|
106
|
+
export const getComparePageURL = (currentLang = 'en') => {
|
|
109
107
|
const lang = currentLang || 'en';
|
|
110
|
-
const comparePageURL = appConfig?.comparePageURL || '/en/navigator/compare';
|
|
111
108
|
|
|
112
|
-
return
|
|
109
|
+
return `/${lang}/navigator/compare`;
|
|
113
110
|
};
|
|
114
111
|
|
|
115
112
|
export const exportComparisonTable = (tools, getToolField) => {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
exportComparisonTable,
|
|
7
7
|
formatFunctionalityScore,
|
|
8
8
|
getComparePageURL,
|
|
9
|
-
|
|
9
|
+
getNavigatorCataloguePageURL,
|
|
10
10
|
rawValueAsArray,
|
|
11
11
|
} from './utils';
|
|
12
12
|
|
|
@@ -86,27 +86,16 @@ describe('Navigator Catalogue utilities', () => {
|
|
|
86
86
|
expect(revokeObjectURL).toHaveBeenCalledWith('blob:test');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it('
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
expect(
|
|
100
|
-
getComparePageURL({ comparePageURL: '/en/tools/compare' }, 'de'),
|
|
101
|
-
).toBe('/de/tools/compare');
|
|
102
|
-
expect(
|
|
103
|
-
getComparePageURL(
|
|
104
|
-
{ landingPageURL: '/en/navigator/tool-catalogue' },
|
|
105
|
-
'de',
|
|
106
|
-
),
|
|
107
|
-
).toBe('/de/navigator/compare');
|
|
108
|
-
expect(getComparePageURL()).toBe('/en/navigator/compare');
|
|
109
|
-
});
|
|
89
|
+
it.each([undefined, '', 'en', 'fr', 'de'])(
|
|
90
|
+
'builds catalogue and comparison URLs for locale %s',
|
|
91
|
+
(locale) => {
|
|
92
|
+
const lang = locale || 'en';
|
|
93
|
+
expect(getNavigatorCataloguePageURL(locale)).toBe(
|
|
94
|
+
`/${lang}/navigator/tool-catalogue`,
|
|
95
|
+
);
|
|
96
|
+
expect(getComparePageURL(locale)).toBe(`/${lang}/navigator/compare`);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
110
99
|
|
|
111
100
|
it('exports every comparison criterion', () => {
|
|
112
101
|
const tools = [
|
|
@@ -6,10 +6,12 @@ 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';
|
|
12
13
|
import useGuideFacetOptions from './useGuideFacetOptions';
|
|
14
|
+
import { getNavigatorCataloguePageURL } from '../../Search/NavigatorCatalogue/utils';
|
|
13
15
|
|
|
14
16
|
const messages = defineMessages({
|
|
15
17
|
noSteps: {
|
|
@@ -177,9 +179,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
177
179
|
allowedFields.has(field),
|
|
178
180
|
);
|
|
179
181
|
const query = new URLManager().stateToUrl({ filters: resultFilters });
|
|
180
|
-
const pathname = (
|
|
181
|
-
appConfig.resultsPageURL || '/en/navigator/tool-catalogue'
|
|
182
|
-
).replace(/^\/en(?=\/|$)/, `/${currentLang}`);
|
|
182
|
+
const pathname = getNavigatorCataloguePageURL(currentLang);
|
|
183
183
|
|
|
184
184
|
history.push({ pathname, search: query ? `?${query}` : '' });
|
|
185
185
|
};
|
|
@@ -351,12 +351,10 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
351
351
|
className="navigator-guide-preview-result"
|
|
352
352
|
key={result._original?._id || result.href}
|
|
353
353
|
>
|
|
354
|
-
<
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
<Icon className="ri-stack-line" />
|
|
359
|
-
</div>
|
|
354
|
+
<ToolThumbnail
|
|
355
|
+
result={result}
|
|
356
|
+
fallbackIcon="ri-stack-line"
|
|
357
|
+
/>
|
|
360
358
|
<div className="navigator-guide-preview-result-content">
|
|
361
359
|
<small
|
|
362
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,10 +4,12 @@ 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,
|
|
10
11
|
getCompareLocation,
|
|
12
|
+
useHasMounted,
|
|
11
13
|
} from './utils';
|
|
12
14
|
|
|
13
15
|
const messages = defineMessages({
|
|
@@ -41,19 +43,21 @@ const messages = defineMessages({
|
|
|
41
43
|
},
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
export const CompareToolsPanel = (
|
|
46
|
+
export const CompareToolsPanel = () => {
|
|
45
47
|
const intl = useIntl();
|
|
46
48
|
const history = useHistory();
|
|
47
49
|
const currentLang = useSelector((state) => state.intl.locale);
|
|
48
|
-
const appConfig = suppliedAppConfig || {
|
|
49
|
-
landingPageURL: '/en/navigator/tool-catalogue',
|
|
50
|
-
};
|
|
51
50
|
const [selectedTools, setSelectedTools] = useAtom(compareToolsAtom);
|
|
52
|
-
const
|
|
51
|
+
const hasMounted = useHasMounted();
|
|
52
|
+
|
|
53
|
+
const effectiveSelectedTools = hasMounted ? selectedTools : [];
|
|
54
|
+
const resolvableSelectedTools = effectiveSelectedTools.filter(
|
|
55
|
+
(tool) => tool.uid,
|
|
56
|
+
);
|
|
53
57
|
const isReadyToCompare = resolvableSelectedTools.length >= 2;
|
|
54
|
-
const emptySlots = MAX_COMPARE_TOOLS -
|
|
58
|
+
const emptySlots = MAX_COMPARE_TOOLS - effectiveSelectedTools.length;
|
|
55
59
|
|
|
56
|
-
if (
|
|
60
|
+
if (!hasMounted || effectiveSelectedTools.length === 0) return null;
|
|
57
61
|
|
|
58
62
|
const removeTool = (toolUid) => {
|
|
59
63
|
setSelectedTools((tools) => tools.filter((tool) => tool.uid !== toolUid));
|
|
@@ -67,7 +71,7 @@ export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
|
|
|
67
71
|
].join('');
|
|
68
72
|
|
|
69
73
|
history.push(
|
|
70
|
-
getCompareLocation(
|
|
74
|
+
getCompareLocation(effectiveSelectedTools, returnURL, currentLang),
|
|
71
75
|
);
|
|
72
76
|
};
|
|
73
77
|
|
|
@@ -77,18 +81,16 @@ export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
|
|
|
77
81
|
<h2>
|
|
78
82
|
{intl.formatMessage(messages.compareTools)}
|
|
79
83
|
<span className="compare-panel-count">
|
|
80
|
-
{
|
|
84
|
+
{effectiveSelectedTools.length}/{MAX_COMPARE_TOOLS}
|
|
81
85
|
</span>
|
|
82
86
|
</h2>
|
|
83
87
|
</div>
|
|
84
88
|
|
|
85
89
|
<div className="compare-panel-content">
|
|
86
90
|
<div className="compare-panel-tools">
|
|
87
|
-
{
|
|
91
|
+
{effectiveSelectedTools.map((tool) => (
|
|
88
92
|
<div key={tool.uid} className="compare-panel-tool">
|
|
89
|
-
<
|
|
90
|
-
<Icon className="ri-file-line" />
|
|
91
|
-
</span>
|
|
93
|
+
<ToolThumbnail result={tool} size="small" />
|
|
92
94
|
<span className="compare-panel-tool-title">{tool.title}</span>
|
|
93
95
|
<Button
|
|
94
96
|
className="compare-panel-tool-clear"
|
|
@@ -3,12 +3,13 @@ import '@testing-library/jest-dom';
|
|
|
3
3
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
5
|
import { Provider as JotaiProvider } from 'jotai';
|
|
6
|
+
import { useSelector } from 'react-redux';
|
|
6
7
|
import { useHistory } from 'react-router-dom';
|
|
7
8
|
import { CompareToolsPanel } from './CompareToolsPanel';
|
|
8
9
|
|
|
9
10
|
jest.mock('react-redux', () => ({
|
|
10
11
|
...jest.requireActual('react-redux'),
|
|
11
|
-
useSelector: (
|
|
12
|
+
useSelector: jest.fn(),
|
|
12
13
|
}));
|
|
13
14
|
|
|
14
15
|
jest.mock('react-router-dom', () => ({
|
|
@@ -16,7 +17,7 @@ jest.mock('react-router-dom', () => ({
|
|
|
16
17
|
useHistory: jest.fn(),
|
|
17
18
|
}));
|
|
18
19
|
|
|
19
|
-
const renderPanel = (selectedTools
|
|
20
|
+
const renderPanel = (selectedTools) => {
|
|
20
21
|
window.localStorage.setItem(
|
|
21
22
|
'cca-compare-tools',
|
|
22
23
|
JSON.stringify(selectedTools),
|
|
@@ -25,7 +26,7 @@ const renderPanel = (selectedTools, appConfig) => {
|
|
|
25
26
|
return render(
|
|
26
27
|
<IntlProvider locale="en">
|
|
27
28
|
<JotaiProvider>
|
|
28
|
-
<CompareToolsPanel
|
|
29
|
+
<CompareToolsPanel />
|
|
29
30
|
</JotaiProvider>
|
|
30
31
|
</IntlProvider>,
|
|
31
32
|
);
|
|
@@ -38,6 +39,8 @@ describe('CompareToolsPanel', () => {
|
|
|
38
39
|
jest.clearAllMocks();
|
|
39
40
|
window.localStorage.clear();
|
|
40
41
|
useHistory.mockReturnValue({ push });
|
|
42
|
+
useSelector.mockReturnValue('en');
|
|
43
|
+
window.history.replaceState({}, '', '/');
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
it('does not render without selected tools', () => {
|
|
@@ -61,14 +64,17 @@ describe('CompareToolsPanel', () => {
|
|
|
61
64
|
expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
|
|
62
65
|
});
|
|
63
66
|
|
|
64
|
-
it('
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ landingPageURL: '/en/navigator/tool-catalogue' },
|
|
67
|
+
it.each(['en', 'fr'])('opens the comparison page in %s', (locale) => {
|
|
68
|
+
useSelector.mockReturnValue(locale);
|
|
69
|
+
window.history.replaceState(
|
|
70
|
+
{},
|
|
71
|
+
'',
|
|
72
|
+
`/${locale}/navigator/tool-catalogue?q=water#results`,
|
|
71
73
|
);
|
|
74
|
+
renderPanel([
|
|
75
|
+
{ uid: 'one', title: 'Tool one' },
|
|
76
|
+
{ uid: 'two', title: 'Tool two' },
|
|
77
|
+
]);
|
|
72
78
|
|
|
73
79
|
expect(screen.getByText('Ready to compare')).toHaveClass(
|
|
74
80
|
'compare-panel-status',
|
|
@@ -78,9 +84,11 @@ describe('CompareToolsPanel', () => {
|
|
|
78
84
|
fireEvent.click(screen.getByText('Compare selected tools'));
|
|
79
85
|
|
|
80
86
|
expect(push).toHaveBeenCalledWith({
|
|
81
|
-
pathname:
|
|
87
|
+
pathname: `/${locale}/navigator/compare`,
|
|
82
88
|
search: '?uid=one&uid=two',
|
|
83
|
-
state: {
|
|
89
|
+
state: {
|
|
90
|
+
returnURL: `/${locale}/navigator/tool-catalogue?q=water#results`,
|
|
91
|
+
},
|
|
84
92
|
});
|
|
85
93
|
});
|
|
86
94
|
|
|
@@ -93,4 +101,58 @@ describe('CompareToolsPanel', () => {
|
|
|
93
101
|
fireEvent.click(screen.getByText('Clear all'));
|
|
94
102
|
expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
|
|
95
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
|
+
});
|
|
96
158
|
});
|