@eeacms/volto-cca-policy 1.0.16 → 1.0.17
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 +14 -0
- package/package.json +1 -1
- 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 +2 -3
- package/src/components/theme/CompareTools/CompareToolsPanel.jsx +13 -10
- package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +20 -12
- package/src/components/theme/CompareTools/CompareToolsView.jsx +24 -25
- package/src/components/theme/CompareTools/CompareToolsView.test.jsx +1 -10
- package/src/components/theme/CompareTools/utils.js +19 -9
- package/src/components/theme/CompareTools/utils.test.jsx +21 -8
- package/src/components/theme/Views/ExtendedToolView.jsx +20 -0
- package/src/components/theme/Views/ExtendedToolView.test.jsx +26 -0
- 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/views.less +22 -31
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.17](https://github.com/eea/volto-cca-policy/compare/1.0.16...1.0.17) - 8 September 2026
|
|
8
|
+
|
|
9
|
+
#### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
- 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)]
|
|
12
|
+
- feat: add MetadataTags component to display tool output types - refs #304612 [kreafox - [`da36a73`](https://github.com/eea/volto-cca-policy/commit/da36a7340da0d36a807b619e1da57d246b11b07d)]
|
|
13
|
+
|
|
14
|
+
#### :nail_care: Enhancements
|
|
15
|
+
|
|
16
|
+
- change: remove sectors from CompareToolsView - refs #304612 [kreafox - [`9b9352f`](https://github.com/eea/volto-cca-policy/commit/9b9352fa116a951ceef64511bc7c75779a7f0ec4)]
|
|
17
|
+
|
|
18
|
+
#### :hammer_and_wrench: Others
|
|
19
|
+
|
|
20
|
+
- test: remove sectors from CompareToolsView accessibility tests [kreafox - [`b3df4e2`](https://github.com/eea/volto-cca-policy/commit/b3df4e21aa79c6d9c726ad498fee2ce9af7f17a0)]
|
|
7
21
|
### [1.0.16](https://github.com/eea/volto-cca-policy/compare/1.0.15...1.0.16) - 4 September 2026
|
|
8
22
|
|
|
9
23
|
### [1.0.15](https://github.com/eea/volto-cca-policy/compare/1.0.14...1.0.15) - 4 September 2026
|
package/package.json
CHANGED
|
@@ -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 = [
|
|
@@ -10,6 +10,7 @@ import guideSteps from '../../../search/navigator_guide/guideSteps';
|
|
|
10
10
|
import { navigatorGuideStepAtom } from '../../../state';
|
|
11
11
|
import { mergeGuideOptions } from './utils';
|
|
12
12
|
import useGuideFacetOptions from './useGuideFacetOptions';
|
|
13
|
+
import { getNavigatorCataloguePageURL } from '../../Search/NavigatorCatalogue/utils';
|
|
13
14
|
|
|
14
15
|
const messages = defineMessages({
|
|
15
16
|
noSteps: {
|
|
@@ -177,9 +178,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
177
178
|
allowedFields.has(field),
|
|
178
179
|
);
|
|
179
180
|
const query = new URLManager().stateToUrl({ filters: resultFilters });
|
|
180
|
-
const pathname = (
|
|
181
|
-
appConfig.resultsPageURL || '/en/navigator/tool-catalogue'
|
|
182
|
-
).replace(/^\/en(?=\/|$)/, `/${currentLang}`);
|
|
181
|
+
const pathname = getNavigatorCataloguePageURL(currentLang);
|
|
183
182
|
|
|
184
183
|
history.push({ pathname, search: query ? `?${query}` : '' });
|
|
185
184
|
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
MAX_COMPARE_TOOLS,
|
|
9
9
|
compareToolsAtom,
|
|
10
10
|
getCompareLocation,
|
|
11
|
+
useHasMounted,
|
|
11
12
|
} from './utils';
|
|
12
13
|
|
|
13
14
|
const messages = defineMessages({
|
|
@@ -41,19 +42,21 @@ const messages = defineMessages({
|
|
|
41
42
|
},
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
export const CompareToolsPanel = (
|
|
45
|
+
export const CompareToolsPanel = () => {
|
|
45
46
|
const intl = useIntl();
|
|
46
47
|
const history = useHistory();
|
|
47
48
|
const currentLang = useSelector((state) => state.intl.locale);
|
|
48
|
-
const appConfig = suppliedAppConfig || {
|
|
49
|
-
landingPageURL: '/en/navigator/tool-catalogue',
|
|
50
|
-
};
|
|
51
49
|
const [selectedTools, setSelectedTools] = useAtom(compareToolsAtom);
|
|
52
|
-
const
|
|
50
|
+
const hasMounted = useHasMounted();
|
|
51
|
+
|
|
52
|
+
const effectiveSelectedTools = hasMounted ? selectedTools : [];
|
|
53
|
+
const resolvableSelectedTools = effectiveSelectedTools.filter(
|
|
54
|
+
(tool) => tool.uid,
|
|
55
|
+
);
|
|
53
56
|
const isReadyToCompare = resolvableSelectedTools.length >= 2;
|
|
54
|
-
const emptySlots = MAX_COMPARE_TOOLS -
|
|
57
|
+
const emptySlots = MAX_COMPARE_TOOLS - effectiveSelectedTools.length;
|
|
55
58
|
|
|
56
|
-
if (
|
|
59
|
+
if (!hasMounted || effectiveSelectedTools.length === 0) return null;
|
|
57
60
|
|
|
58
61
|
const removeTool = (toolUid) => {
|
|
59
62
|
setSelectedTools((tools) => tools.filter((tool) => tool.uid !== toolUid));
|
|
@@ -67,7 +70,7 @@ export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
|
|
|
67
70
|
].join('');
|
|
68
71
|
|
|
69
72
|
history.push(
|
|
70
|
-
getCompareLocation(
|
|
73
|
+
getCompareLocation(effectiveSelectedTools, returnURL, currentLang),
|
|
71
74
|
);
|
|
72
75
|
};
|
|
73
76
|
|
|
@@ -77,14 +80,14 @@ export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
|
|
|
77
80
|
<h2>
|
|
78
81
|
{intl.formatMessage(messages.compareTools)}
|
|
79
82
|
<span className="compare-panel-count">
|
|
80
|
-
{
|
|
83
|
+
{effectiveSelectedTools.length}/{MAX_COMPARE_TOOLS}
|
|
81
84
|
</span>
|
|
82
85
|
</h2>
|
|
83
86
|
</div>
|
|
84
87
|
|
|
85
88
|
<div className="compare-panel-content">
|
|
86
89
|
<div className="compare-panel-tools">
|
|
87
|
-
{
|
|
90
|
+
{effectiveSelectedTools.map((tool) => (
|
|
88
91
|
<div key={tool.uid} className="compare-panel-tool">
|
|
89
92
|
<span className="navigator-tool-icon small" aria-hidden="true">
|
|
90
93
|
<Icon className="ri-file-line" />
|
|
@@ -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
|
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
asArray,
|
|
29
29
|
exportComparisonTable,
|
|
30
30
|
formatFunctionalityScore,
|
|
31
|
-
|
|
31
|
+
getNavigatorCataloguePageURL,
|
|
32
32
|
} from '../../Search/NavigatorCatalogue/utils';
|
|
33
33
|
|
|
34
34
|
const messages = defineMessages({
|
|
@@ -99,10 +99,6 @@ const messages = defineMessages({
|
|
|
99
99
|
id: 'Adaptation support cycle step',
|
|
100
100
|
defaultMessage: 'Adaptation support cycle step',
|
|
101
101
|
},
|
|
102
|
-
sector: {
|
|
103
|
-
id: 'Sector',
|
|
104
|
-
defaultMessage: 'Sector',
|
|
105
|
-
},
|
|
106
102
|
});
|
|
107
103
|
|
|
108
104
|
const getToolField = (tool, field) =>
|
|
@@ -111,6 +107,25 @@ const getToolField = (tool, field) =>
|
|
|
111
107
|
const getToolFieldDisplay = (tool, field) =>
|
|
112
108
|
asArray(getToolField(tool, field)).join(', ') || '—';
|
|
113
109
|
|
|
110
|
+
const MetadataTags = ({ value }) => {
|
|
111
|
+
const intl = useIntl();
|
|
112
|
+
const items = asArray(value)
|
|
113
|
+
.map((item) => item?.title || item?.token || item)
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
|
|
116
|
+
return items.length ? (
|
|
117
|
+
<div className="metadata-tags">
|
|
118
|
+
{items.map((item, index) => (
|
|
119
|
+
<span className="metadata-tag" key={`${item}-${index}`}>
|
|
120
|
+
{intl.formatMessage({ id: item, defaultMessage: item })}
|
|
121
|
+
</span>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
'—'
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
114
129
|
const FieldValueList = ({ value, label }) => {
|
|
115
130
|
const values = asArray(value);
|
|
116
131
|
|
|
@@ -205,8 +220,7 @@ const CompareToolsView = () => {
|
|
|
205
220
|
[location.search],
|
|
206
221
|
);
|
|
207
222
|
const registry = config.settings.searchlib;
|
|
208
|
-
const
|
|
209
|
-
const landingPageURL = getLocalizedLandingPageURL(appConfig, currentLang);
|
|
223
|
+
const landingPageURL = getNavigatorCataloguePageURL(currentLang);
|
|
210
224
|
const compareToolsTitle = intl.formatMessage(messages.compareTools);
|
|
211
225
|
const returnURL =
|
|
212
226
|
location.state?.returnURL ||
|
|
@@ -462,7 +476,9 @@ const CompareToolsView = () => {
|
|
|
462
476
|
</Table.Cell>
|
|
463
477
|
{visibleTools.map((tool) => (
|
|
464
478
|
<Table.Cell key={`output-type-${tool.id}`}>
|
|
465
|
-
|
|
479
|
+
<MetadataTags
|
|
480
|
+
value={getToolField(tool, 'cca_type_of_outputs')}
|
|
481
|
+
/>
|
|
466
482
|
</Table.Cell>
|
|
467
483
|
))}
|
|
468
484
|
</Table.Row>
|
|
@@ -488,23 +504,6 @@ const CompareToolsView = () => {
|
|
|
488
504
|
</Table.Cell>
|
|
489
505
|
))}
|
|
490
506
|
</Table.Row>
|
|
491
|
-
<Table.Row>
|
|
492
|
-
<Table.Cell as="th" scope="row">
|
|
493
|
-
<div className="compare-criteria">
|
|
494
|
-
<div className="compare-criteria-title">
|
|
495
|
-
{intl.formatMessage(messages.sector)}
|
|
496
|
-
</div>
|
|
497
|
-
</div>
|
|
498
|
-
</Table.Cell>
|
|
499
|
-
{visibleTools.map((tool) => (
|
|
500
|
-
<Table.Cell key={`sector-${tool.id}`}>
|
|
501
|
-
<FieldValueList
|
|
502
|
-
label={intl.formatMessage(messages.sector)}
|
|
503
|
-
value={getToolField(tool, 'cca_adaptation_sectors')}
|
|
504
|
-
/>
|
|
505
|
-
</Table.Cell>
|
|
506
|
-
))}
|
|
507
|
-
</Table.Row>
|
|
508
507
|
</Table.Body>
|
|
509
508
|
</Table>
|
|
510
509
|
)}
|
|
@@ -41,9 +41,7 @@ jest.mock('@plone/volto/registry', () => ({
|
|
|
41
41
|
settings: {
|
|
42
42
|
searchlib: {
|
|
43
43
|
searchui: {
|
|
44
|
-
navigatorCatalogueSearch: {
|
|
45
|
-
landingPageURL: '/en/navigator/tool-catalogue',
|
|
46
|
-
},
|
|
44
|
+
navigatorCatalogueSearch: {},
|
|
47
45
|
},
|
|
48
46
|
},
|
|
49
47
|
},
|
|
@@ -80,9 +78,6 @@ describe('CompareToolsView accessibility', () => {
|
|
|
80
78
|
cca_adaptation_support_cycle_step: {
|
|
81
79
|
raw: [{ title: 'Assessing risks' }, { title: 'Monitoring' }],
|
|
82
80
|
},
|
|
83
|
-
cca_adaptation_sectors: {
|
|
84
|
-
raw: [{ title: 'Agriculture' }, { title: 'Water management' }],
|
|
85
|
-
},
|
|
86
81
|
},
|
|
87
82
|
{
|
|
88
83
|
cca_uid: { raw: 'two' },
|
|
@@ -125,9 +120,5 @@ describe('CompareToolsView accessibility', () => {
|
|
|
125
120
|
expect(adaptationSteps).toHaveTextContent('Assessing risks');
|
|
126
121
|
expect(adaptationSteps).toHaveTextContent('Monitoring');
|
|
127
122
|
expect(adaptationSteps.children).toHaveLength(2);
|
|
128
|
-
const sectors = screen.getByRole('list', { name: 'Sector' });
|
|
129
|
-
expect(sectors).toHaveTextContent('Agriculture');
|
|
130
|
-
expect(sectors).toHaveTextContent('Water management');
|
|
131
|
-
expect(sectors.children).toHaveLength(2);
|
|
132
123
|
});
|
|
133
124
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
1
2
|
import { useAtom } from 'jotai';
|
|
2
3
|
import { atomWithStorage } from 'jotai/utils';
|
|
3
4
|
import { applyConfigurationSchema, rebind } from '@eeacms/search';
|
|
@@ -9,6 +10,16 @@ export const MAX_COMPARE_TOOLS = 4;
|
|
|
9
10
|
export const compareToolsAtom = atomWithStorage('cca-compare-tools', []);
|
|
10
11
|
const searchAppName = 'navigatorCatalogueSearch';
|
|
11
12
|
|
|
13
|
+
export const useHasMounted = () => {
|
|
14
|
+
const [hasMounted, setHasMounted] = useState(false);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setHasMounted(true);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return hasMounted;
|
|
21
|
+
};
|
|
22
|
+
|
|
12
23
|
const getRawValue = (value) =>
|
|
13
24
|
value?.raw !== undefined ? value.raw : value || '';
|
|
14
25
|
|
|
@@ -27,11 +38,14 @@ export const getPathname = (url) => {
|
|
|
27
38
|
|
|
28
39
|
export const useCompareTools = (compareTool) => {
|
|
29
40
|
const [selectedTools, setSelectedTools] = useAtom(compareToolsAtom);
|
|
30
|
-
const
|
|
41
|
+
const hasMounted = useHasMounted();
|
|
42
|
+
|
|
43
|
+
const effectiveSelectedTools = hasMounted ? selectedTools : [];
|
|
44
|
+
const isSelected = effectiveSelectedTools.some(
|
|
31
45
|
(tool) => tool.uid === compareTool?.uid,
|
|
32
46
|
);
|
|
33
47
|
const isLimitReached =
|
|
34
|
-
|
|
48
|
+
effectiveSelectedTools.length >= MAX_COMPARE_TOOLS && !isSelected;
|
|
35
49
|
|
|
36
50
|
const setSelected = (selected) => {
|
|
37
51
|
if (!compareTool?.uid) return;
|
|
@@ -57,15 +71,11 @@ export const useCompareTools = (compareTool) => {
|
|
|
57
71
|
isLimitReached,
|
|
58
72
|
setSelected,
|
|
59
73
|
toggle: () => setSelected(!isSelected),
|
|
74
|
+
hasMounted,
|
|
60
75
|
};
|
|
61
76
|
};
|
|
62
77
|
|
|
63
|
-
export const getCompareLocation = (
|
|
64
|
-
tools,
|
|
65
|
-
appConfig,
|
|
66
|
-
returnURL,
|
|
67
|
-
currentLang = 'en',
|
|
68
|
-
) => {
|
|
78
|
+
export const getCompareLocation = (tools, returnURL, currentLang = 'en') => {
|
|
69
79
|
const params = new URLSearchParams();
|
|
70
80
|
|
|
71
81
|
tools.slice(0, MAX_COMPARE_TOOLS).forEach((tool) => {
|
|
@@ -75,7 +85,7 @@ export const getCompareLocation = (
|
|
|
75
85
|
});
|
|
76
86
|
|
|
77
87
|
const query = params.toString();
|
|
78
|
-
const path = getComparePageURL(
|
|
88
|
+
const path = getComparePageURL(currentLang);
|
|
79
89
|
|
|
80
90
|
return {
|
|
81
91
|
pathname: path,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getCompareToolUid,
|
|
13
13
|
getPathname,
|
|
14
14
|
useCompareTools,
|
|
15
|
+
useHasMounted,
|
|
15
16
|
} from './utils';
|
|
16
17
|
|
|
17
18
|
jest.mock('@eeacms/search', () => ({
|
|
@@ -26,11 +27,12 @@ jest.mock('@plone/volto/helpers/Url/Url', () => ({
|
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
29
|
const CompareHarness = ({ tool }) => {
|
|
29
|
-
const { isLimitReached, isSelected, setSelected, toggle } =
|
|
30
|
+
const { hasMounted, isLimitReached, isSelected, setSelected, toggle } =
|
|
30
31
|
useCompareTools(tool);
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<>
|
|
35
|
+
<span data-testid="mounted">{String(hasMounted)}</span>
|
|
34
36
|
<span data-testid="selected">{String(isSelected)}</span>
|
|
35
37
|
<span data-testid="limit">{String(isLimitReached)}</span>
|
|
36
38
|
<button type="button" onClick={toggle}>
|
|
@@ -95,19 +97,14 @@ describe('Compare Tools utilities', () => {
|
|
|
95
97
|
];
|
|
96
98
|
|
|
97
99
|
expect(
|
|
98
|
-
getCompareLocation(
|
|
99
|
-
tools,
|
|
100
|
-
{ landingPageURL: '/en/navigator/tool-catalogue' },
|
|
101
|
-
'/en/navigator/tool-catalogue?q=test',
|
|
102
|
-
'ro',
|
|
103
|
-
),
|
|
100
|
+
getCompareLocation(tools, '/en/navigator/tool-catalogue?q=test', 'ro'),
|
|
104
101
|
).toEqual({
|
|
105
102
|
pathname: '/ro/navigator/compare',
|
|
106
103
|
search: '?uid=one&uid=two&uid=three',
|
|
107
104
|
state: { returnURL: '/en/navigator/tool-catalogue?q=test' },
|
|
108
105
|
});
|
|
109
106
|
expect(MAX_COMPARE_TOOLS).toBe(4);
|
|
110
|
-
expect(getCompareLocation([]
|
|
107
|
+
expect(getCompareLocation([])).toEqual({
|
|
111
108
|
pathname: '/en/navigator/compare',
|
|
112
109
|
search: '',
|
|
113
110
|
state: { returnURL: undefined },
|
|
@@ -214,4 +211,20 @@ describe('Compare Tools utilities', () => {
|
|
|
214
211
|
fireEvent.click(screen.getByText('Select'));
|
|
215
212
|
expect(screen.getByTestId('selected')).toHaveTextContent('false');
|
|
216
213
|
});
|
|
214
|
+
|
|
215
|
+
it('exposes hasMounted state to safely coordinate client hydration', () => {
|
|
216
|
+
renderCompareHarness({ uid: 'one', title: 'Tool one' });
|
|
217
|
+
|
|
218
|
+
expect(screen.getByTestId('mounted')).toHaveTextContent('true');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('tracks mounting status with useHasMounted', () => {
|
|
222
|
+
const MountedComponent = () => {
|
|
223
|
+
const mounted = useHasMounted();
|
|
224
|
+
return <div data-testid="mounted-flag">{String(mounted)}</div>;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
render(<MountedComponent />);
|
|
228
|
+
expect(screen.getByTestId('mounted-flag')).toHaveTextContent('true');
|
|
229
|
+
});
|
|
217
230
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { Button, Container, Grid, Icon, Segment } from 'semantic-ui-react';
|
|
3
|
+
import { useHistory } from 'react-router-dom';
|
|
4
|
+
import { useSelector } from 'react-redux';
|
|
3
5
|
import {
|
|
4
6
|
CompareToolsPanel,
|
|
5
7
|
ExtendedToolGeographicMetadata,
|
|
@@ -12,6 +14,7 @@ import { FormattedMessage } from 'react-intl';
|
|
|
12
14
|
import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
|
|
13
15
|
import useClipboard from '@plone/volto/hooks/clipboard/useClipboard';
|
|
14
16
|
import { useCompareTools } from '../CompareTools/utils';
|
|
17
|
+
import { getNavigatorCataloguePageURL } from '../../Search/NavigatorCatalogue/utils';
|
|
15
18
|
import RelatedTools from './RelatedTools';
|
|
16
19
|
|
|
17
20
|
const ExtendedToolView = (props) => {
|
|
@@ -55,6 +58,8 @@ const ExtendedToolView = (props) => {
|
|
|
55
58
|
const hasGeoChars = Boolean(geochars || spatial_layer?.length);
|
|
56
59
|
const hasHyperlink = Boolean(hyperlink && hyperlink.length > 0);
|
|
57
60
|
const hasCompareTool = Boolean(content.UID);
|
|
61
|
+
const history = useHistory();
|
|
62
|
+
const currentLang = useSelector((state) => state.intl.locale);
|
|
58
63
|
|
|
59
64
|
const compareTool = {
|
|
60
65
|
uid: content.UID,
|
|
@@ -75,6 +80,10 @@ const ExtendedToolView = (props) => {
|
|
|
75
80
|
return () => clearTimeout(timeout);
|
|
76
81
|
}, [isLinkCopied, setIsLinkCopied]);
|
|
77
82
|
|
|
83
|
+
const checkNavigatorCatalogue = () => {
|
|
84
|
+
history.push(getNavigatorCataloguePageURL(currentLang));
|
|
85
|
+
};
|
|
86
|
+
|
|
78
87
|
return (
|
|
79
88
|
<div className="extended-tool-view">
|
|
80
89
|
<BodyClass className="extended-tool" />
|
|
@@ -226,6 +235,17 @@ const ExtendedToolView = (props) => {
|
|
|
226
235
|
className="col-right"
|
|
227
236
|
>
|
|
228
237
|
<Segment className="metadata">
|
|
238
|
+
<Button
|
|
239
|
+
className="primary inverted"
|
|
240
|
+
onClick={checkNavigatorCatalogue}
|
|
241
|
+
>
|
|
242
|
+
<Icon className="ri-arrow-left-line" />
|
|
243
|
+
<FormattedMessage
|
|
244
|
+
id="Check Navigator Catalogue"
|
|
245
|
+
defaultMessage="Check Navigator Catalogue"
|
|
246
|
+
/>
|
|
247
|
+
</Button>
|
|
248
|
+
|
|
229
249
|
<h4 className="metadata-header">Metadata</h4>
|
|
230
250
|
|
|
231
251
|
{intended_user_groups?.length > 0 && (
|
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import '@testing-library/jest-dom';
|
|
3
3
|
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
|
+
import { useSelector } from 'react-redux';
|
|
6
|
+
import { useHistory } from 'react-router-dom';
|
|
5
7
|
|
|
6
8
|
import ExtendedToolView from './ExtendedToolView';
|
|
7
9
|
import { useCompareTools } from '../CompareTools/utils';
|
|
@@ -11,8 +13,18 @@ jest.mock('../CompareTools/utils', () => ({
|
|
|
11
13
|
useCompareTools: jest.fn(),
|
|
12
14
|
}));
|
|
13
15
|
|
|
16
|
+
jest.mock('react-redux', () => ({
|
|
17
|
+
...jest.requireActual('react-redux'),
|
|
18
|
+
useSelector: jest.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
14
21
|
jest.mock('@plone/volto/hooks/clipboard/useClipboard', () => jest.fn());
|
|
15
22
|
|
|
23
|
+
jest.mock('react-router-dom', () => ({
|
|
24
|
+
...jest.requireActual('react-router-dom'),
|
|
25
|
+
useHistory: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
16
28
|
jest.mock(
|
|
17
29
|
'@plone/volto/components/manage/UniversalLink/UniversalLink',
|
|
18
30
|
() =>
|
|
@@ -51,6 +63,7 @@ jest.mock('@eeacms/volto-cca-policy/components', () => ({
|
|
|
51
63
|
const toggle = jest.fn();
|
|
52
64
|
const copyShareUrl = jest.fn();
|
|
53
65
|
const setIsLinkCopied = jest.fn();
|
|
66
|
+
const push = jest.fn();
|
|
54
67
|
|
|
55
68
|
const renderComponent = (content = {}) =>
|
|
56
69
|
render(
|
|
@@ -69,6 +82,19 @@ describe('ExtendedToolView', () => {
|
|
|
69
82
|
toggle,
|
|
70
83
|
});
|
|
71
84
|
useClipboard.mockReturnValue([false, copyShareUrl, setIsLinkCopied]);
|
|
85
|
+
useHistory.mockReturnValue({ push });
|
|
86
|
+
useSelector.mockReturnValue('en');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it.each(['en', 'fr'])('opens the Navigator Catalogue in %s', (locale) => {
|
|
90
|
+
useSelector.mockReturnValue(locale);
|
|
91
|
+
renderComponent({ title: 'Climate Tool' });
|
|
92
|
+
|
|
93
|
+
fireEvent.click(
|
|
94
|
+
screen.getByRole('button', { name: /check navigator catalogue/i }),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(push).toHaveBeenCalledWith(`/${locale}/navigator/tool-catalogue`);
|
|
72
98
|
});
|
|
73
99
|
|
|
74
100
|
it('renders the title', () => {
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
|
|
1
3
|
export default function withClientOnly(WrappedComponent) {
|
|
2
4
|
const WithClientWrappedComponent = (props) => {
|
|
3
|
-
|
|
5
|
+
const [hasMounted, setHasMounted] = useState(false);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setHasMounted(true);
|
|
9
|
+
}, []);
|
|
10
|
+
|
|
11
|
+
if (!hasMounted) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <WrappedComponent {...props} />;
|
|
4
16
|
};
|
|
5
17
|
|
|
6
18
|
return WithClientWrappedComponent;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import withClientOnly from './withClientOnly';
|
|
5
|
+
|
|
6
|
+
describe('withClientOnly', () => {
|
|
7
|
+
it('renders the wrapped component after mount', () => {
|
|
8
|
+
const Component = (props) => (
|
|
9
|
+
<div data-testid="wrapped-content">{props.text}</div>
|
|
10
|
+
);
|
|
11
|
+
const ClientOnlyComponent = withClientOnly(Component);
|
|
12
|
+
|
|
13
|
+
render(<ClientOnlyComponent text="Client Hello" />);
|
|
14
|
+
|
|
15
|
+
expect(screen.getByTestId('wrapped-content')).toHaveTextContent(
|
|
16
|
+
'Client Hello',
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -8,7 +8,6 @@ const previewResultsLimit = 3;
|
|
|
8
8
|
|
|
9
9
|
const navigatorGuideConfig = {
|
|
10
10
|
title: 'Navigator Guide',
|
|
11
|
-
landingPageURL: '/en/navigator',
|
|
12
11
|
alwaysSearchOnInitialLoad: true,
|
|
13
12
|
resultsPerPage: previewResultsLimit,
|
|
14
13
|
previewResultsLimit,
|
|
@@ -16,7 +15,6 @@ const navigatorGuideConfig = {
|
|
|
16
15
|
showLandingPage: false,
|
|
17
16
|
layoutComponent: 'NavigatorGuideLayout',
|
|
18
17
|
contentBodyComponent: 'NavigatorGuideContentView',
|
|
19
|
-
resultsPageURL: '/en/navigator/tool-catalogue',
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
export default function installNavigatorGuideSearch(config) {
|
package/theme/globals/views.less
CHANGED
|
@@ -481,7 +481,6 @@ hr {
|
|
|
481
481
|
}
|
|
482
482
|
|
|
483
483
|
.metadata-header {
|
|
484
|
-
margin: 0;
|
|
485
484
|
padding-bottom: 0.75rem;
|
|
486
485
|
border-bottom: 1px dashed #dde3e9;
|
|
487
486
|
}
|
|
@@ -519,28 +518,6 @@ hr {
|
|
|
519
518
|
}
|
|
520
519
|
}
|
|
521
520
|
|
|
522
|
-
.metadata-tags {
|
|
523
|
-
display: flex;
|
|
524
|
-
flex-wrap: wrap;
|
|
525
|
-
gap: 0.5rem;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
.metadata-tag {
|
|
529
|
-
padding: 0.35rem 0.75rem;
|
|
530
|
-
border: 1px solid #d5dde5;
|
|
531
|
-
border-radius: 2px;
|
|
532
|
-
color: #4c5a6b;
|
|
533
|
-
font-weight: 600;
|
|
534
|
-
line-height: 1.25;
|
|
535
|
-
font-size: 14px;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
.metadata-tag-more {
|
|
539
|
-
background: transparent;
|
|
540
|
-
cursor: pointer;
|
|
541
|
-
font-family: inherit;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
521
|
.extended-tool-related {
|
|
545
522
|
padding: 2.5rem;
|
|
546
523
|
margin-top: 3rem;
|
|
@@ -663,14 +640,6 @@ hr {
|
|
|
663
640
|
gap: 1rem;
|
|
664
641
|
}
|
|
665
642
|
|
|
666
|
-
// .extended-tool-related-reason {
|
|
667
|
-
// padding: 0.3rem 0.6rem;
|
|
668
|
-
// background: #e4f2d8;
|
|
669
|
-
// color: #0a7b6c;
|
|
670
|
-
// font-size: 12px;
|
|
671
|
-
// font-weight: 500;
|
|
672
|
-
// }
|
|
673
|
-
|
|
674
643
|
.extended-tool-related-view {
|
|
675
644
|
display: inline-flex;
|
|
676
645
|
align-items: center;
|
|
@@ -697,6 +666,28 @@ hr {
|
|
|
697
666
|
}
|
|
698
667
|
}
|
|
699
668
|
|
|
669
|
+
.metadata-tags {
|
|
670
|
+
display: flex;
|
|
671
|
+
flex-wrap: wrap;
|
|
672
|
+
gap: 0.5rem;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.metadata-tag {
|
|
676
|
+
padding: 0.35rem 0.75rem;
|
|
677
|
+
border: 1px solid #d5dde5;
|
|
678
|
+
border-radius: 2px;
|
|
679
|
+
color: #4c5a6b;
|
|
680
|
+
font-weight: 600;
|
|
681
|
+
line-height: 1.25;
|
|
682
|
+
font-size: 14px;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.metadata-tag-more {
|
|
686
|
+
background: transparent;
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
font-family: inherit;
|
|
689
|
+
}
|
|
690
|
+
|
|
700
691
|
.extended-tool {
|
|
701
692
|
.ui.segment.breadcrumbs {
|
|
702
693
|
background-color: #f7f8fa;
|