@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
|
@@ -17,6 +17,7 @@ import { GET_BREADCRUMBS } from '@plone/volto/constants/ActionTypes';
|
|
|
17
17
|
import config from '@plone/volto/registry';
|
|
18
18
|
import { defineMessages, useIntl } from 'react-intl';
|
|
19
19
|
import BannerTitle from '../BannerTitle/BannerTitle';
|
|
20
|
+
import ToolThumbnail from '../ToolThumbnail/ToolThumbnail';
|
|
20
21
|
import {
|
|
21
22
|
MAX_COMPARE_TOOLS,
|
|
22
23
|
compareToolsAtom,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
asArray,
|
|
29
30
|
exportComparisonTable,
|
|
30
31
|
formatFunctionalityScore,
|
|
31
|
-
|
|
32
|
+
getNavigatorCataloguePageURL,
|
|
32
33
|
} from '../../Search/NavigatorCatalogue/utils';
|
|
33
34
|
|
|
34
35
|
const messages = defineMessages({
|
|
@@ -99,10 +100,6 @@ const messages = defineMessages({
|
|
|
99
100
|
id: 'Adaptation support cycle step',
|
|
100
101
|
defaultMessage: 'Adaptation support cycle step',
|
|
101
102
|
},
|
|
102
|
-
sector: {
|
|
103
|
-
id: 'Sector',
|
|
104
|
-
defaultMessage: 'Sector',
|
|
105
|
-
},
|
|
106
103
|
});
|
|
107
104
|
|
|
108
105
|
const getToolField = (tool, field) =>
|
|
@@ -111,6 +108,25 @@ const getToolField = (tool, field) =>
|
|
|
111
108
|
const getToolFieldDisplay = (tool, field) =>
|
|
112
109
|
asArray(getToolField(tool, field)).join(', ') || '—';
|
|
113
110
|
|
|
111
|
+
const MetadataTags = ({ value }) => {
|
|
112
|
+
const intl = useIntl();
|
|
113
|
+
const items = asArray(value)
|
|
114
|
+
.map((item) => item?.title || item?.token || item)
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
|
|
117
|
+
return items.length ? (
|
|
118
|
+
<div className="metadata-tags">
|
|
119
|
+
{items.map((item, index) => (
|
|
120
|
+
<span className="metadata-tag" key={`${item}-${index}`}>
|
|
121
|
+
{intl.formatMessage({ id: item, defaultMessage: item })}
|
|
122
|
+
</span>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
'—'
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
114
130
|
const FieldValueList = ({ value, label }) => {
|
|
115
131
|
const values = asArray(value);
|
|
116
132
|
|
|
@@ -205,8 +221,7 @@ const CompareToolsView = () => {
|
|
|
205
221
|
[location.search],
|
|
206
222
|
);
|
|
207
223
|
const registry = config.settings.searchlib;
|
|
208
|
-
const
|
|
209
|
-
const landingPageURL = getLocalizedLandingPageURL(appConfig, currentLang);
|
|
224
|
+
const landingPageURL = getNavigatorCataloguePageURL(currentLang);
|
|
210
225
|
const compareToolsTitle = intl.formatMessage(messages.compareTools);
|
|
211
226
|
const returnURL =
|
|
212
227
|
location.state?.returnURL ||
|
|
@@ -368,12 +383,7 @@ const CompareToolsView = () => {
|
|
|
368
383
|
<Table.HeaderCell key={tool.id}>
|
|
369
384
|
<div className="compare-tool-header">
|
|
370
385
|
<div className="compare-tool-title-row">
|
|
371
|
-
<
|
|
372
|
-
className="navigator-tool-icon medium"
|
|
373
|
-
aria-hidden="true"
|
|
374
|
-
>
|
|
375
|
-
<Icon className="ri-file-line" />
|
|
376
|
-
</div>
|
|
386
|
+
<ToolThumbnail result={tool.result} />
|
|
377
387
|
<div className="compare-tool-title" title={tool.title}>
|
|
378
388
|
{tool.title}
|
|
379
389
|
</div>
|
|
@@ -462,7 +472,9 @@ const CompareToolsView = () => {
|
|
|
462
472
|
</Table.Cell>
|
|
463
473
|
{visibleTools.map((tool) => (
|
|
464
474
|
<Table.Cell key={`output-type-${tool.id}`}>
|
|
465
|
-
|
|
475
|
+
<MetadataTags
|
|
476
|
+
value={getToolField(tool, 'cca_type_of_outputs')}
|
|
477
|
+
/>
|
|
466
478
|
</Table.Cell>
|
|
467
479
|
))}
|
|
468
480
|
</Table.Row>
|
|
@@ -488,23 +500,6 @@ const CompareToolsView = () => {
|
|
|
488
500
|
</Table.Cell>
|
|
489
501
|
))}
|
|
490
502
|
</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
503
|
</Table.Body>
|
|
509
504
|
</Table>
|
|
510
505
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import '@testing-library/jest-dom';
|
|
3
|
-
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
5
|
import { useAtom } from 'jotai';
|
|
6
6
|
import { useDispatch, useSelector } from 'react-redux';
|
|
@@ -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
|
},
|
|
@@ -58,7 +56,7 @@ jest.mock('./utils', () => ({
|
|
|
58
56
|
getPathname: (url) => url?.split('?')[0] || '',
|
|
59
57
|
}));
|
|
60
58
|
|
|
61
|
-
describe('CompareToolsView
|
|
59
|
+
describe('CompareToolsView', () => {
|
|
62
60
|
beforeEach(() => {
|
|
63
61
|
jest.clearAllMocks();
|
|
64
62
|
useAtom.mockReturnValue([[], jest.fn()]);
|
|
@@ -76,18 +74,19 @@ describe('CompareToolsView accessibility', () => {
|
|
|
76
74
|
cca_uid: { raw: 'one' },
|
|
77
75
|
title: 'Tool one',
|
|
78
76
|
href: '/tool-one',
|
|
77
|
+
image: {
|
|
78
|
+
scales: { thumb: { download: '/uploaded-compare-thumb.jpg' } },
|
|
79
|
+
},
|
|
79
80
|
functionality: { raw: 4 },
|
|
80
81
|
cca_adaptation_support_cycle_step: {
|
|
81
82
|
raw: [{ title: 'Assessing risks' }, { title: 'Monitoring' }],
|
|
82
83
|
},
|
|
83
|
-
cca_adaptation_sectors: {
|
|
84
|
-
raw: [{ title: 'Agriculture' }, { title: 'Water management' }],
|
|
85
|
-
},
|
|
86
84
|
},
|
|
87
85
|
{
|
|
88
86
|
cca_uid: { raw: 'two' },
|
|
89
87
|
title: 'Tool two',
|
|
90
88
|
href: '/tool-two',
|
|
89
|
+
image: null,
|
|
91
90
|
},
|
|
92
91
|
]);
|
|
93
92
|
});
|
|
@@ -125,9 +124,45 @@ describe('CompareToolsView accessibility', () => {
|
|
|
125
124
|
expect(adaptationSteps).toHaveTextContent('Assessing risks');
|
|
126
125
|
expect(adaptationSteps).toHaveTextContent('Monitoring');
|
|
127
126
|
expect(adaptationSteps.children).toHaveLength(2);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('shows the uploaded tool result image and keeps the file icon for a missing image', async () => {
|
|
130
|
+
render(
|
|
131
|
+
<IntlProvider locale="en">
|
|
132
|
+
<CompareToolsView />
|
|
133
|
+
</IntlProvider>,
|
|
134
|
+
);
|
|
135
|
+
const table = await screen.findByRole('table', { name: 'Compare tools' });
|
|
136
|
+
const [thumbnail, missingThumbnail] = table.querySelectorAll(
|
|
137
|
+
'.navigator-tool-icon',
|
|
138
|
+
);
|
|
139
|
+
const img = thumbnail.querySelector('img');
|
|
140
|
+
|
|
141
|
+
expect(thumbnail).toHaveClass('medium');
|
|
142
|
+
expect(img).toHaveAttribute('src', '/uploaded-compare-thumb.jpg');
|
|
143
|
+
expect(img).toHaveStyle({ display: 'none' });
|
|
144
|
+
expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
|
|
145
|
+
expect(missingThumbnail.querySelector('img')).not.toBeInTheDocument();
|
|
146
|
+
expect(missingThumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
|
|
147
|
+
|
|
148
|
+
fireEvent.load(img);
|
|
149
|
+
|
|
150
|
+
expect(img.style.display).toBe('');
|
|
151
|
+
expect(thumbnail.querySelector('.ri-file-line')).not.toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('falls back to the file icon when the image fails', async () => {
|
|
155
|
+
render(
|
|
156
|
+
<IntlProvider locale="en">
|
|
157
|
+
<CompareToolsView />
|
|
158
|
+
</IntlProvider>,
|
|
159
|
+
);
|
|
160
|
+
const table = await screen.findByRole('table', { name: 'Compare tools' });
|
|
161
|
+
const thumbnail = table.querySelector('.navigator-tool-icon');
|
|
162
|
+
|
|
163
|
+
fireEvent.error(thumbnail.querySelector('img'));
|
|
164
|
+
|
|
165
|
+
expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
|
|
166
|
+
expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
|
|
132
167
|
});
|
|
133
168
|
});
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Icon } from 'semantic-ui-react';
|
|
3
|
+
import Image from '@plone/volto/components/theme/Image/Image';
|
|
4
|
+
import { getToolThumbnailUrl } from './utils';
|
|
5
|
+
|
|
6
|
+
const ToolThumbnail = ({
|
|
7
|
+
result,
|
|
8
|
+
size = 'medium',
|
|
9
|
+
fallbackIcon = 'ri-file-line',
|
|
10
|
+
}) => {
|
|
11
|
+
const [imageLoaded, setImageLoaded] = useState(false);
|
|
12
|
+
const [hasImageError, setHasImageError] = useState(false);
|
|
13
|
+
const thumbUrl = getToolThumbnailUrl(result);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setImageLoaded(false);
|
|
17
|
+
setHasImageError(false);
|
|
18
|
+
}, [thumbUrl]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={`navigator-tool-icon ${size}`} aria-hidden="true">
|
|
22
|
+
{thumbUrl && !hasImageError ? (
|
|
23
|
+
<>
|
|
24
|
+
<Image
|
|
25
|
+
src={thumbUrl}
|
|
26
|
+
alt=""
|
|
27
|
+
style={imageLoaded ? undefined : { display: 'none' }}
|
|
28
|
+
onLoad={() => setImageLoaded(true)}
|
|
29
|
+
onError={() => setHasImageError(true)}
|
|
30
|
+
/>
|
|
31
|
+
{!imageLoaded && <Icon className={fallbackIcon} />}
|
|
32
|
+
</>
|
|
33
|
+
) : (
|
|
34
|
+
<Icon className={fallbackIcon} />
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default ToolThumbnail;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
4
|
+
import ToolThumbnail from './ToolThumbnail';
|
|
5
|
+
|
|
6
|
+
describe('ToolThumbnail', () => {
|
|
7
|
+
it.each(['load', 'error'])(
|
|
8
|
+
'resets the %s state when the thumbnail URL changes',
|
|
9
|
+
(event) => {
|
|
10
|
+
const { container, rerender } = render(
|
|
11
|
+
<ToolThumbnail result={{ image: '/first.jpg' }} />,
|
|
12
|
+
);
|
|
13
|
+
fireEvent[event](container.querySelector('img'));
|
|
14
|
+
|
|
15
|
+
rerender(<ToolThumbnail result={{ image: '/second.jpg' }} />);
|
|
16
|
+
|
|
17
|
+
const img = container.querySelector('img');
|
|
18
|
+
expect(img).toHaveAttribute('src', '/second.jpg');
|
|
19
|
+
expect(img).toHaveAttribute('alt', '');
|
|
20
|
+
expect(img).toHaveStyle({ display: 'none' });
|
|
21
|
+
expect(container.querySelector('.ri-file-line')).toBeInTheDocument();
|
|
22
|
+
|
|
23
|
+
fireEvent.load(img);
|
|
24
|
+
|
|
25
|
+
expect(img.style.display).toBe('');
|
|
26
|
+
expect(container.querySelector('.ri-file-line')).not.toBeInTheDocument();
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
2
|
+
|
|
3
|
+
export function getToolThumbnailUrl(result) {
|
|
4
|
+
if (!result) return null;
|
|
5
|
+
|
|
6
|
+
if (result.image === null || result.image === false) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof result.image === 'string' && result.image) {
|
|
11
|
+
return flattenToAppURL(result.image);
|
|
12
|
+
}
|
|
13
|
+
if (result.image && typeof result.image === 'object') {
|
|
14
|
+
const scaleUrl =
|
|
15
|
+
result.image.scales?.thumb?.download ||
|
|
16
|
+
result.image.scales?.tile?.download ||
|
|
17
|
+
result.image.scales?.preview?.download ||
|
|
18
|
+
result.image.scales?.mini?.download ||
|
|
19
|
+
result.image.download;
|
|
20
|
+
if (scaleUrl) {
|
|
21
|
+
return flattenToAppURL(scaleUrl);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
typeof result.thumbUrl === 'string' &&
|
|
27
|
+
result.thumbUrl &&
|
|
28
|
+
!result.thumbUrl.includes('portal_depiction')
|
|
29
|
+
) {
|
|
30
|
+
return flattenToAppURL(result.thumbUrl);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
result.image_preview &&
|
|
35
|
+
typeof result.image_preview.raw === 'string' &&
|
|
36
|
+
result.image_preview.raw
|
|
37
|
+
) {
|
|
38
|
+
return flattenToAppURL(result.image_preview.raw);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const href =
|
|
42
|
+
result.href || result['@id'] || result.about?.raw || result.about;
|
|
43
|
+
if (href && typeof href === 'string') {
|
|
44
|
+
const cleanHref = flattenToAppURL(href).replace(/\/+$/, '');
|
|
45
|
+
return `${cleanHref}/@@images/image/thumb`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getToolThumbnailUrl } from './utils';
|
|
2
|
+
|
|
3
|
+
describe('getToolThumbnailUrl', () => {
|
|
4
|
+
it.each([null, false])(
|
|
5
|
+
'does not resolve other URLs when image is %s',
|
|
6
|
+
(image) => {
|
|
7
|
+
expect(
|
|
8
|
+
getToolThumbnailUrl({
|
|
9
|
+
image,
|
|
10
|
+
thumbUrl: '/thumb.jpg',
|
|
11
|
+
image_preview: { raw: '/preview.jpg' },
|
|
12
|
+
href: '/tools/tool',
|
|
13
|
+
}),
|
|
14
|
+
).toBeNull();
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
it.each(['thumb', 'tile', 'preview', 'mini'])(
|
|
19
|
+
'prefers the %s scale to the original download',
|
|
20
|
+
(scale) => {
|
|
21
|
+
expect(
|
|
22
|
+
getToolThumbnailUrl({
|
|
23
|
+
image: {
|
|
24
|
+
scales: { [scale]: { download: '/scaled.jpg' } },
|
|
25
|
+
download: '/original.jpg',
|
|
26
|
+
},
|
|
27
|
+
thumbUrl: '/thumb.jpg',
|
|
28
|
+
}),
|
|
29
|
+
).toBe('/scaled.jpg');
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
it('uses the raw image preview before constructing a traversal URL', () => {
|
|
34
|
+
expect(
|
|
35
|
+
getToolThumbnailUrl({
|
|
36
|
+
thumbUrl: '/portal_depiction/tool/image_preview',
|
|
37
|
+
image_preview: { raw: '/preview.jpg' },
|
|
38
|
+
href: '/tools/tool',
|
|
39
|
+
}),
|
|
40
|
+
).toBe('/preview.jpg');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns null for empty or invalid results', () => {
|
|
44
|
+
expect(getToolThumbnailUrl(null)).toBeNull();
|
|
45
|
+
expect(getToolThumbnailUrl(undefined)).toBeNull();
|
|
46
|
+
expect(getToolThumbnailUrl({})).toBeNull();
|
|
47
|
+
expect(getToolThumbnailUrl({ image: null })).toBeNull();
|
|
48
|
+
expect(getToolThumbnailUrl({ image: false })).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('resolves direct image string URL', () => {
|
|
52
|
+
expect(
|
|
53
|
+
getToolThumbnailUrl({ image: '/metadata/tools/tool/image.png' }),
|
|
54
|
+
).toBe('/metadata/tools/tool/image.png');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('resolves image scales from Dexterity image field', () => {
|
|
58
|
+
expect(
|
|
59
|
+
getToolThumbnailUrl({
|
|
60
|
+
image: {
|
|
61
|
+
scales: {
|
|
62
|
+
thumb: { download: '/image-128.jpeg' },
|
|
63
|
+
tile: { download: '/image-64.jpeg' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
).toBe('/image-128.jpeg');
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
getToolThumbnailUrl({
|
|
71
|
+
image: {
|
|
72
|
+
scales: {
|
|
73
|
+
tile: { download: '/image-64.jpeg' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
).toBe('/image-64.jpeg');
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
getToolThumbnailUrl({
|
|
81
|
+
image: {
|
|
82
|
+
download: '/image-original.jpeg',
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
).toBe('/image-original.jpeg');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('resolves thumbUrl when not portal_depiction fallback', () => {
|
|
89
|
+
expect(
|
|
90
|
+
getToolThumbnailUrl({
|
|
91
|
+
thumbUrl: '/custom-thumb.jpg',
|
|
92
|
+
}),
|
|
93
|
+
).toBe('/custom-thumb.jpg');
|
|
94
|
+
|
|
95
|
+
// portal_depiction is ignored, falls back to href scale
|
|
96
|
+
expect(
|
|
97
|
+
getToolThumbnailUrl({
|
|
98
|
+
thumbUrl:
|
|
99
|
+
'https://www.eea.europa.eu/portal_depiction/tool/image_preview',
|
|
100
|
+
href: '/tools/sample-tool',
|
|
101
|
+
}),
|
|
102
|
+
).toBe('/tools/sample-tool/@@images/image/thumb');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('constructs scale traversal URL from href, @id, or about', () => {
|
|
106
|
+
expect(
|
|
107
|
+
getToolThumbnailUrl({
|
|
108
|
+
href: '/tools/sample-tool/',
|
|
109
|
+
}),
|
|
110
|
+
).toBe('/tools/sample-tool/@@images/image/thumb');
|
|
111
|
+
|
|
112
|
+
expect(
|
|
113
|
+
getToolThumbnailUrl({
|
|
114
|
+
'@id': '/tools/sample-tool',
|
|
115
|
+
}),
|
|
116
|
+
).toBe('/tools/sample-tool/@@images/image/thumb');
|
|
117
|
+
|
|
118
|
+
expect(
|
|
119
|
+
getToolThumbnailUrl({
|
|
120
|
+
about: { raw: '/tools/sample-tool' },
|
|
121
|
+
}),
|
|
122
|
+
).toBe('/tools/sample-tool/@@images/image/thumb');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -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,8 @@ 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 { getToolThumbnailUrl } from '../ToolThumbnail/utils';
|
|
18
|
+
import { getNavigatorCataloguePageURL } from '../../Search/NavigatorCatalogue/utils';
|
|
15
19
|
import RelatedTools from './RelatedTools';
|
|
16
20
|
|
|
17
21
|
const ExtendedToolView = (props) => {
|
|
@@ -55,11 +59,14 @@ const ExtendedToolView = (props) => {
|
|
|
55
59
|
const hasGeoChars = Boolean(geochars || spatial_layer?.length);
|
|
56
60
|
const hasHyperlink = Boolean(hyperlink && hyperlink.length > 0);
|
|
57
61
|
const hasCompareTool = Boolean(content.UID);
|
|
62
|
+
const history = useHistory();
|
|
63
|
+
const currentLang = useSelector((state) => state.intl.locale);
|
|
58
64
|
|
|
59
65
|
const compareTool = {
|
|
60
66
|
uid: content.UID,
|
|
61
67
|
title,
|
|
62
68
|
href: content['@id'],
|
|
69
|
+
image: getToolThumbnailUrl(content),
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
const { isSelected, isLimitReached, toggle } = useCompareTools(compareTool);
|
|
@@ -75,6 +82,10 @@ const ExtendedToolView = (props) => {
|
|
|
75
82
|
return () => clearTimeout(timeout);
|
|
76
83
|
}, [isLinkCopied, setIsLinkCopied]);
|
|
77
84
|
|
|
85
|
+
const checkNavigatorCatalogue = () => {
|
|
86
|
+
history.push(getNavigatorCataloguePageURL(currentLang));
|
|
87
|
+
};
|
|
88
|
+
|
|
78
89
|
return (
|
|
79
90
|
<div className="extended-tool-view">
|
|
80
91
|
<BodyClass className="extended-tool" />
|
|
@@ -226,6 +237,17 @@ const ExtendedToolView = (props) => {
|
|
|
226
237
|
className="col-right"
|
|
227
238
|
>
|
|
228
239
|
<Segment className="metadata">
|
|
240
|
+
<Button
|
|
241
|
+
className="primary inverted"
|
|
242
|
+
onClick={checkNavigatorCatalogue}
|
|
243
|
+
>
|
|
244
|
+
<Icon className="ri-arrow-left-line" />
|
|
245
|
+
<FormattedMessage
|
|
246
|
+
id="Check Navigator Catalogue"
|
|
247
|
+
defaultMessage="Check Navigator Catalogue"
|
|
248
|
+
/>
|
|
249
|
+
</Button>
|
|
250
|
+
|
|
229
251
|
<h4 className="metadata-header">Metadata</h4>
|
|
230
252
|
|
|
231
253
|
{intended_user_groups?.length > 0 && (
|