@eeacms/volto-cca-policy 1.0.1 → 1.0.3

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 (47) hide show
  1. package/.github/workflows/betterleaks.yml +133 -0
  2. package/.gitleaks.toml +89 -0
  3. package/CHANGELOG.md +262 -1
  4. package/README.md +59 -0
  5. package/RELEASE.md +1 -1
  6. package/jest-addon.config.js +7 -3
  7. package/package.json +3 -2
  8. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +228 -0
  9. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +167 -0
  10. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +189 -0
  11. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.jsx +11 -0
  12. package/src/components/Search/NavigatorCatalogue/utils.js +101 -0
  13. package/src/components/Search/NavigatorCatalogue/utils.test.js +132 -0
  14. package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +409 -0
  15. package/src/components/Search/NavigatorGuide/NavigatorGuideLayout.jsx +8 -0
  16. package/src/components/index.js +2 -0
  17. package/src/components/manage/Blocks/CollectionStatistics/CollectionStatsView.test.jsx +2 -0
  18. package/src/components/theme/CompareTools/CompareToolsPanel.jsx +151 -0
  19. package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +96 -0
  20. package/src/components/theme/CompareTools/CompareToolsView.jsx +468 -0
  21. package/src/components/theme/CompareTools/utils.js +111 -0
  22. package/src/components/theme/CompareTools/utils.test.jsx +217 -0
  23. package/src/components/theme/Views/ExtendedToolView.jsx +409 -0
  24. package/src/components/theme/Views/ExtendedToolView.test.jsx +436 -0
  25. package/src/constants.js +1 -0
  26. package/src/customizations/@plone-collective/volto-rss-provider/express-middleware.js +1 -1
  27. package/src/customizations/volto/components/theme/View/LinkView.jsx +1 -1
  28. package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.js +1 -1
  29. package/src/customizations/volto/server.jsx +1 -1
  30. package/src/helpers/Utils.jsx +29 -0
  31. package/src/helpers/index.js +3 -0
  32. package/src/index.js +31 -2
  33. package/src/search/index.js +6 -0
  34. package/src/search/navigator_catalogue/config.js +89 -0
  35. package/src/search/navigator_catalogue/facets.js +86 -0
  36. package/src/search/navigator_catalogue/views.js +28 -0
  37. package/src/search/navigator_guide/config.js +70 -0
  38. package/src/search/navigator_guide/facets.js +31 -0
  39. package/src/search/navigator_guide/guideSteps.js +93 -0
  40. package/src/slate-styles.less +4 -0
  41. package/src/state.js +1 -0
  42. package/theme/elements/button.overrides +0 -11
  43. package/theme/elements/label.overrides +15 -0
  44. package/theme/globals/blocks.less +3 -2
  45. package/theme/globals/navigator.less +967 -0
  46. package/theme/globals/site.overrides +5 -0
  47. package/theme/tokens/colors.less +2 -0
@@ -0,0 +1,101 @@
1
+ export const asArray = (value) => {
2
+ if (!value) return [];
3
+ const raw = value.raw !== undefined ? value.raw : value;
4
+ if (!raw) return [];
5
+ const values = Array.isArray(raw)
6
+ ? raw.filter(Boolean)
7
+ : [raw].filter(Boolean);
8
+
9
+ return values.map((value) => value?.title || value?.token || value);
10
+ };
11
+
12
+ export const arrayFieldToString = (value) => asArray(value).join(', ');
13
+
14
+ export const formatFunctionalityScore = (value) => {
15
+ const raw = value?.raw !== undefined ? value.raw : value;
16
+ const score = raw === null || raw === undefined ? Number.NaN : Number(raw);
17
+
18
+ return Number.isFinite(score) ? `${Math.min(6, Math.max(0, score))}/6` : '—';
19
+ };
20
+
21
+ export const escapeCsvValue = (value) => {
22
+ const stringValue = value === undefined || value === null ? '' : `${value}`;
23
+ return `"${stringValue.replace(/"/g, '""')}"`;
24
+ };
25
+
26
+ export const downloadCsv = (filename, rows) => {
27
+ const csv = rows.map((row) => row.map(escapeCsvValue).join(',')).join('\n');
28
+
29
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
30
+ const url = URL.createObjectURL(blob);
31
+ const link = document.createElement('a');
32
+
33
+ link.href = url;
34
+ link.setAttribute('download', filename);
35
+ document.body.appendChild(link);
36
+ link.click();
37
+ link.remove();
38
+
39
+ URL.revokeObjectURL(url);
40
+ };
41
+
42
+ export const getLocalizedLandingPageURL = (appConfig, currentLang = 'en') => {
43
+ const lang = currentLang || 'en';
44
+ const landingPageURL =
45
+ appConfig?.landingPageURL || '/en/navigator/tool-catalogue';
46
+
47
+ return landingPageURL.replace(/^\/en(?=\/|$)/, `/${lang}`);
48
+ };
49
+
50
+ export const getComparePageURL = (appConfig, currentLang = 'en') => {
51
+ const lang = currentLang || 'en';
52
+ const comparePageURL = appConfig?.comparePageURL || '/en/navigator/compare';
53
+
54
+ return comparePageURL.replace(/^\/en(?=\/|$)/, `/${lang}`);
55
+ };
56
+
57
+ export const exportComparisonTable = (tools, getToolField) => {
58
+ const rows = [
59
+ ['Criteria', ...tools.map((tool) => tool.title)],
60
+ [
61
+ 'Usability',
62
+ ...tools.map((tool) =>
63
+ arrayFieldToString(getToolField(tool, 'accessibility_and_usability')),
64
+ ),
65
+ ],
66
+ [
67
+ 'Functionality',
68
+ ...tools.map((tool) =>
69
+ formatFunctionalityScore(getToolField(tool, 'functionality')),
70
+ ),
71
+ ],
72
+ [
73
+ 'Spatial scale',
74
+ ...tools.map((tool) =>
75
+ arrayFieldToString(getToolField(tool, 'cca_geographical_scale')),
76
+ ),
77
+ ],
78
+ [
79
+ 'Output type',
80
+ ...tools.map((tool) =>
81
+ arrayFieldToString(getToolField(tool, 'cca_type_of_outputs')),
82
+ ),
83
+ ],
84
+ [
85
+ 'Adaptation support cycle step',
86
+ ...tools.map((tool) =>
87
+ arrayFieldToString(
88
+ getToolField(tool, 'cca_adaptation_support_cycle_step'),
89
+ ),
90
+ ),
91
+ ],
92
+ [
93
+ 'Sector',
94
+ ...tools.map((tool) =>
95
+ arrayFieldToString(getToolField(tool, 'cca_adaptation_sectors')),
96
+ ),
97
+ ],
98
+ ];
99
+
100
+ downloadCsv('navigator-catalogue-comparison.csv', rows);
101
+ };
@@ -0,0 +1,132 @@
1
+ import {
2
+ arrayFieldToString,
3
+ asArray,
4
+ downloadCsv,
5
+ escapeCsvValue,
6
+ exportComparisonTable,
7
+ formatFunctionalityScore,
8
+ getComparePageURL,
9
+ getLocalizedLandingPageURL,
10
+ } from './utils';
11
+
12
+ describe('Navigator Catalogue utilities', () => {
13
+ let clickSpy;
14
+ let createObjectURL;
15
+ let originalBlob;
16
+ let revokeObjectURL;
17
+
18
+ beforeEach(() => {
19
+ originalBlob = global.Blob;
20
+ global.Blob = jest.fn((parts, options) => ({ parts, options }));
21
+ clickSpy = jest
22
+ .spyOn(HTMLAnchorElement.prototype, 'click')
23
+ .mockImplementation(() => {});
24
+ createObjectURL = jest.fn(() => 'blob:test');
25
+ revokeObjectURL = jest.fn();
26
+ Object.defineProperty(URL, 'createObjectURL', {
27
+ configurable: true,
28
+ value: createObjectURL,
29
+ });
30
+ Object.defineProperty(URL, 'revokeObjectURL', {
31
+ configurable: true,
32
+ value: revokeObjectURL,
33
+ });
34
+ });
35
+
36
+ afterEach(() => {
37
+ global.Blob = originalBlob;
38
+ clickSpy.mockRestore();
39
+ jest.restoreAllMocks();
40
+ });
41
+
42
+ it('normalizes raw, scalar, title, token, and empty values', () => {
43
+ expect(asArray()).toEqual([]);
44
+ expect(asArray({ raw: null })).toEqual([]);
45
+ expect(asArray({ raw: ['One', null, { title: 'Two' }] })).toEqual([
46
+ 'One',
47
+ 'Two',
48
+ ]);
49
+ expect(asArray({ token: 'TOKEN' })).toEqual(['TOKEN']);
50
+ expect(asArray('Value')).toEqual(['Value']);
51
+ expect(arrayFieldToString({ raw: ['One', 'Two'] })).toBe('One, Two');
52
+ });
53
+
54
+ it('formats functionality scores and clamps invalid ranges', () => {
55
+ expect(formatFunctionalityScore({ raw: 4 })).toBe('4/6');
56
+ expect(formatFunctionalityScore(-2)).toBe('0/6');
57
+ expect(formatFunctionalityScore(9)).toBe('6/6');
58
+ expect(formatFunctionalityScore(null)).toBe('—');
59
+ expect(formatFunctionalityScore('invalid')).toBe('—');
60
+ });
61
+
62
+ it('escapes CSV values', () => {
63
+ expect(escapeCsvValue()).toBe('""');
64
+ expect(escapeCsvValue(null)).toBe('""');
65
+ expect(escapeCsvValue('A "quoted" value')).toBe('"A ""quoted"" value"');
66
+ });
67
+
68
+ it('downloads CSV data and cleans up the temporary link', () => {
69
+ const appendSpy = jest.spyOn(document.body, 'appendChild');
70
+ const removeSpy = jest.spyOn(HTMLAnchorElement.prototype, 'remove');
71
+
72
+ downloadCsv('comparison.csv', [
73
+ ['Title', 'Tool'],
74
+ ['Sector', 'Water'],
75
+ ]);
76
+
77
+ expect(createObjectURL).toHaveBeenCalledTimes(1);
78
+ expect(appendSpy).toHaveBeenCalledWith(expect.any(HTMLAnchorElement));
79
+ expect(clickSpy).toHaveBeenCalledTimes(1);
80
+ expect(removeSpy).toHaveBeenCalledTimes(1);
81
+ expect(revokeObjectURL).toHaveBeenCalledWith('blob:test');
82
+ });
83
+
84
+ it('localizes catalogue and comparison URLs', () => {
85
+ expect(getLocalizedLandingPageURL(undefined)).toBe(
86
+ '/en/navigator/tool-catalogue',
87
+ );
88
+ expect(
89
+ getLocalizedLandingPageURL({ landingPageURL: '/en/tools' }, 'fr'),
90
+ ).toBe('/fr/tools');
91
+ expect(getLocalizedLandingPageURL({ landingPageURL: '/tools' }, '')).toBe(
92
+ '/tools',
93
+ );
94
+ expect(
95
+ getComparePageURL({ comparePageURL: '/en/tools/compare' }, 'de'),
96
+ ).toBe('/de/tools/compare');
97
+ expect(
98
+ getComparePageURL(
99
+ { landingPageURL: '/en/navigator/tool-catalogue' },
100
+ 'de',
101
+ ),
102
+ ).toBe('/de/navigator/compare');
103
+ expect(getComparePageURL()).toBe('/en/navigator/compare');
104
+ });
105
+
106
+ it('exports every comparison criterion', () => {
107
+ const tools = [
108
+ {
109
+ title: 'Tool A',
110
+ values: {
111
+ accessibility_and_usability: 'High',
112
+ functionality: 5,
113
+ cca_geographical_scale: ['Local'],
114
+ cca_type_of_outputs: ['Maps'],
115
+ cca_adaptation_support_cycle_step: ['Step 1'],
116
+ cca_adaptation_sectors: ['Water'],
117
+ },
118
+ },
119
+ ];
120
+
121
+ exportComparisonTable(tools, (tool, field) => tool.values[field]);
122
+
123
+ const csv = global.Blob.mock.calls[0][0][0];
124
+ expect(csv).toContain('"Criteria","Tool A"');
125
+ expect(csv).toContain('"Usability","High"');
126
+ expect(csv).toContain('"Functionality","5/6"');
127
+ expect(csv).toContain('"Spatial scale","Local"');
128
+ expect(csv).toContain('"Output type","Maps"');
129
+ expect(csv).toContain('"Adaptation support cycle step","Step 1"');
130
+ expect(csv).toContain('"Sector","Water"');
131
+ });
132
+ });
@@ -0,0 +1,409 @@
1
+ import React from 'react';
2
+ import { useAtom } from 'jotai';
3
+ import { useSelector } from 'react-redux';
4
+ import { useHistory } from 'react-router-dom';
5
+ import { defineMessages, useIntl } from 'react-intl';
6
+ import { Button, Checkbox, Icon, Loader, Message } from 'semantic-ui-react';
7
+ import URLManager from '@elastic/search-ui/lib/cjs/URLManager';
8
+ import { useSearchContext } from '@eeacms/search/lib/hocs';
9
+ import guideSteps from '../../../search/navigator_guide/guideSteps';
10
+ import { navigatorGuideStepAtom } from '../../../state';
11
+
12
+ const messages = defineMessages({
13
+ noSteps: {
14
+ id: 'No Navigator Guide steps are configured.',
15
+ defaultMessage: 'No Navigator Guide steps are configured.',
16
+ },
17
+ guideMe: {
18
+ id: 'Guide me',
19
+ defaultMessage: 'Guide me',
20
+ },
21
+ findTheRightTool: {
22
+ id: 'Find the right tool',
23
+ defaultMessage: 'Find the right tool',
24
+ },
25
+ introduction: {
26
+ id: 'Navigator Guide introduction',
27
+ defaultMessage:
28
+ 'Answer a few guided questions. The catalogue on the right narrows as you go - you can skip to the results at any point.',
29
+ },
30
+ stepProgress: {
31
+ id: 'Step {current} of {total}',
32
+ defaultMessage: 'Step {current} of {total}',
33
+ },
34
+ selectAllThatApply: {
35
+ id: 'Select all that apply',
36
+ defaultMessage: 'Select all that apply',
37
+ },
38
+ noOptions: {
39
+ id: 'No options are available for this step.',
40
+ defaultMessage: 'No options are available for this step.',
41
+ },
42
+ back: {
43
+ id: 'Back',
44
+ defaultMessage: 'Back',
45
+ },
46
+ skipToResults: {
47
+ id: 'Skip to results',
48
+ defaultMessage: 'Skip to results',
49
+ },
50
+ seeResults: {
51
+ id: 'See results',
52
+ defaultMessage: 'See results',
53
+ },
54
+ nextStep: {
55
+ id: 'Next step',
56
+ defaultMessage: 'Next step',
57
+ },
58
+ livePreview: {
59
+ id: 'Live preview · results based on your selection',
60
+ defaultMessage: 'Live preview · results based on your selection',
61
+ },
62
+ previewEmpty: {
63
+ id: 'Navigator Guide preview empty state',
64
+ defaultMessage:
65
+ 'Start by selecting the categories that interest you. The tools filtered by your chosen categories will appear in this box.',
66
+ },
67
+ toolsMatch: {
68
+ id: '{count, plural, one {tool matches} other {tools match}}',
69
+ defaultMessage: '{count, plural, one {tool matches} other {tools match}}',
70
+ },
71
+ refinedBy: {
72
+ id: 'Refined by {criteria}',
73
+ defaultMessage: 'Refined by {criteria}',
74
+ },
75
+ seeAllMatchingTools: {
76
+ id: 'See all matching tools',
77
+ defaultMessage: 'See all matching tools',
78
+ },
79
+ });
80
+
81
+ const getFacetOptions = (facets, field) =>
82
+ (facets?.[field]?.[0]?.data || []).map(({ value, count }) => ({
83
+ value,
84
+ count,
85
+ }));
86
+
87
+ const isStepSelected = (filters, field) =>
88
+ (filters || []).some(
89
+ (filter) => filter.field === field && filter.values?.length,
90
+ );
91
+
92
+ const previewTagTypes = {
93
+ adaptationSectors: 'sector',
94
+ climateImpacts: 'hazard',
95
+ adaptationStage: 'adaptation-stage',
96
+ coverage: 'coverage',
97
+ };
98
+
99
+ const NavigatorGuideContentView = ({ appConfig }) => {
100
+ const intl = useIntl();
101
+ const history = useHistory();
102
+ const currentLang = useSelector((state) => state.intl.locale || 'en');
103
+ const searchContext = useSearchContext();
104
+ const {
105
+ addFilter,
106
+ facets,
107
+ filters,
108
+ isLoading,
109
+ removeFilter,
110
+ results,
111
+ totalResults,
112
+ } = searchContext;
113
+ const steps = guideSteps;
114
+ const [storedActiveStep, setActiveStep] = useAtom(navigatorGuideStepAtom);
115
+ const activeStep =
116
+ Number.isInteger(storedActiveStep) &&
117
+ storedActiveStep >= 0 &&
118
+ storedActiveStep < steps.length
119
+ ? storedActiveStep
120
+ : 0;
121
+ const step = steps[activeStep];
122
+ const selectedValues =
123
+ (filters || []).find((filter) => filter.field === step?.field)?.values ||
124
+ [];
125
+ const options = getFacetOptions(facets, step?.field);
126
+ const isLastStep = activeStep === steps.length - 1;
127
+ const hasSelections = steps.some(({ field }) =>
128
+ isStepSelected(filters, field),
129
+ );
130
+ const selectedStepLabels = steps
131
+ .filter(({ field }) => isStepSelected(filters, field))
132
+ .map(({ label }) =>
133
+ intl.formatMessage(label).toLocaleLowerCase(currentLang),
134
+ );
135
+ const selectedPreviewTags = steps.flatMap((item) => {
136
+ const values =
137
+ (filters || []).find((filter) => filter.field === item.field)?.values ||
138
+ [];
139
+
140
+ return values.map((value) => ({
141
+ label: item.id === 'adaptationStage' ? value.split(':')[0] : value,
142
+ type: previewTagTypes[item.id],
143
+ }));
144
+ });
145
+ const visiblePreviewTags = selectedPreviewTags.slice(0, 3);
146
+ const remainingPreviewTags =
147
+ selectedPreviewTags.length - visiblePreviewTags.length;
148
+
149
+ React.useEffect(() => {
150
+ if (storedActiveStep !== activeStep) {
151
+ setActiveStep(activeStep);
152
+ }
153
+ }, [activeStep, setActiveStep, storedActiveStep]);
154
+
155
+ const toggleValue = (value) => {
156
+ if (selectedValues.includes(value)) {
157
+ removeFilter(step.field, value, step.filterType);
158
+ } else {
159
+ addFilter(step.field, value, step.filterType);
160
+ }
161
+ };
162
+
163
+ const showResults = () => {
164
+ const allowedFields = new Set([
165
+ 'language',
166
+ ...steps.map(({ field }) => field),
167
+ ]);
168
+ const resultFilters = (filters || []).filter(({ field }) =>
169
+ allowedFields.has(field),
170
+ );
171
+ const query = new URLManager().stateToUrl({ filters: resultFilters });
172
+ const pathname = (
173
+ appConfig.resultsPageURL || '/en/navigator/tool-catalogue'
174
+ ).replace(/^\/en(?=\/|$)/, `/${currentLang}`);
175
+
176
+ history.push({ pathname, search: query ? `?${query}` : '' });
177
+ };
178
+
179
+ if (!step) {
180
+ return <Message warning>{intl.formatMessage(messages.noSteps)}</Message>;
181
+ }
182
+
183
+ return (
184
+ <div className="navigator-guide-search">
185
+ <header className="navigator-guide-header">
186
+ <div className="navigator-guide-eyebrow">
187
+ {intl.formatMessage(messages.guideMe)}
188
+ </div>
189
+ <h2>
190
+ {appConfig.headline || intl.formatMessage(messages.findTheRightTool)}
191
+ </h2>
192
+ <p>
193
+ {appConfig.subheadline || intl.formatMessage(messages.introduction)}
194
+ </p>
195
+ </header>
196
+
197
+ <div className="navigator-guide-layout">
198
+ <section className="navigator-guide-wizard">
199
+ <div className="navigator-guide-progress">
200
+ {steps.map((item, index) => (
201
+ <React.Fragment key={item.id}>
202
+ <Button
203
+ className={`navigator-guide-progress-step${
204
+ index === activeStep ? ' active' : ''
205
+ }${index < activeStep ? ' completed' : ''}`}
206
+ aria-current={index === activeStep ? 'step' : undefined}
207
+ onClick={() => setActiveStep(index)}
208
+ >
209
+ <span>
210
+ {isStepSelected(filters, item.field) ? (
211
+ <Icon className="ri-check-line" />
212
+ ) : (
213
+ index + 1
214
+ )}
215
+ </span>
216
+ {intl.formatMessage(item.label)}
217
+ </Button>
218
+ {index < steps.length - 1 && (
219
+ <span
220
+ className={`navigator-guide-progress-connector${
221
+ index < activeStep ? ' completed' : ''
222
+ }`}
223
+ aria-hidden="true"
224
+ />
225
+ )}
226
+ </React.Fragment>
227
+ ))}
228
+ </div>
229
+ <div
230
+ className="navigator-guide-progress-bar"
231
+ role="progressbar"
232
+ aria-valuemin="1"
233
+ aria-valuemax={steps.length}
234
+ aria-valuenow={activeStep + 1}
235
+ >
236
+ <div
237
+ className="navigator-guide-progress-bar-fill"
238
+ style={{ width: `${((activeStep + 1) / steps.length) * 100}%` }}
239
+ />
240
+ </div>
241
+
242
+ <div className="navigator-guide-step-meta">
243
+ <span className="navigator-guide-step-number">
244
+ {intl.formatMessage(messages.stepProgress, {
245
+ current: activeStep + 1,
246
+ total: steps.length,
247
+ })}
248
+ </span>
249
+ <span>{intl.formatMessage(messages.selectAllThatApply)}</span>
250
+ </div>
251
+ <h3>{intl.formatMessage(step.title)}</h3>
252
+ {step.description && <p>{intl.formatMessage(step.description)}</p>}
253
+
254
+ {isLoading ? (
255
+ <div className="navigator-guide-options-loading">
256
+ <Loader active inline />
257
+ </div>
258
+ ) : options.length > 0 ? (
259
+ <div className="navigator-guide-options">
260
+ {options.map((option) => (
261
+ <label
262
+ key={option.value}
263
+ className={`navigator-guide-option${
264
+ selectedValues.includes(option.value) ? ' selected' : ''
265
+ }`}
266
+ >
267
+ <Checkbox
268
+ checked={selectedValues.includes(option.value)}
269
+ onChange={() => toggleValue(option.value)}
270
+ />
271
+ <span>{option.value}</span>
272
+ <small>{option.count}</small>
273
+ </label>
274
+ ))}
275
+ </div>
276
+ ) : (
277
+ <Message>{intl.formatMessage(messages.noOptions)}</Message>
278
+ )}
279
+
280
+ <div className="navigator-guide-actions">
281
+ <Button
282
+ disabled={activeStep === 0}
283
+ onClick={() => setActiveStep((value) => value - 1)}
284
+ >
285
+ <Icon className="ri-arrow-left-line" />
286
+ {intl.formatMessage(messages.back)}
287
+ </Button>
288
+ <div>
289
+ <Button className="primary inverted" onClick={showResults}>
290
+ {intl.formatMessage(messages.skipToResults)}
291
+ </Button>
292
+ <Button
293
+ labelPosition="right"
294
+ className="primary icon"
295
+ onClick={() =>
296
+ isLastStep
297
+ ? showResults()
298
+ : setActiveStep((value) => value + 1)
299
+ }
300
+ >
301
+ {intl.formatMessage(
302
+ isLastStep ? messages.seeResults : messages.nextStep,
303
+ )}
304
+ <Icon className="ri-arrow-right-line" />
305
+ </Button>
306
+ </div>
307
+ </div>
308
+ </section>
309
+
310
+ <aside className="navigator-guide-preview">
311
+ <div className="navigator-guide-preview-header">
312
+ <Icon className="ri-stack-line" />
313
+ <div className="navigator-guide-preview-label">
314
+ {intl.formatMessage(messages.livePreview)}
315
+ </div>
316
+ </div>
317
+ <div className="navigator-guide-result-count">
318
+ <span className="navigator-guide-result-count-value">
319
+ {isLoading ? '…' : totalResults || 0}
320
+ </span>
321
+ <p>
322
+ {intl.formatMessage(messages.toolsMatch, {
323
+ count: totalResults || 0,
324
+ })}
325
+ </p>
326
+ </div>
327
+ {selectedStepLabels.length > 0 && (
328
+ <p className="navigator-guide-preview-refinements">
329
+ {intl.formatMessage(messages.refinedBy, {
330
+ criteria: selectedStepLabels.join(' · '),
331
+ })}
332
+ </p>
333
+ )}
334
+ {hasSelections ? (
335
+ <>
336
+ <div className="navigator-guide-preview-results">
337
+ {(results || [])
338
+ .slice(0, appConfig.previewResultsLimit)
339
+ .map((result) => (
340
+ <div
341
+ className="navigator-guide-preview-result"
342
+ key={result._original?._id || result.href}
343
+ >
344
+ <div
345
+ className="navigator-tool-icon medium"
346
+ aria-hidden="true"
347
+ >
348
+ <Icon className="ri-stack-line" />
349
+ </div>
350
+ <div className="navigator-guide-preview-result-content">
351
+ <small
352
+ className="navigator-tool-provider"
353
+ title={result?._result?.tool_provider?.raw}
354
+ >
355
+ {result?._result?.tool_provider?.raw}
356
+ </small>
357
+ <a
358
+ href={result.href}
359
+ target="_blank"
360
+ rel="noopener noreferrer"
361
+ className="navigator-guide-preview-result-title"
362
+ title={result.title}
363
+ >
364
+ <h5>{result.title}</h5>
365
+ </a>
366
+ {selectedPreviewTags.length > 0 && (
367
+ <div className="navigator-guide-preview-tags">
368
+ {visiblePreviewTags.map(
369
+ ({ label, type }, index) => (
370
+ <span
371
+ className={`navigator-tag ${type}`}
372
+ key={`${type}-${label}-${index}`}
373
+ >
374
+ {label}
375
+ </span>
376
+ ),
377
+ )}
378
+ {remainingPreviewTags > 0 && (
379
+ <span className="navigator-guide-preview-tag-more">
380
+ + {remainingPreviewTags}
381
+ </span>
382
+ )}
383
+ </div>
384
+ )}
385
+ </div>
386
+ </div>
387
+ ))}
388
+ </div>
389
+ <Button
390
+ labelPosition="right"
391
+ className="primary icon inverted"
392
+ onClick={showResults}
393
+ >
394
+ {intl.formatMessage(messages.seeAllMatchingTools)}
395
+ <Icon className="ri-arrow-right-line" />
396
+ </Button>
397
+ </>
398
+ ) : (
399
+ <p className="navigator-guide-preview-empty">
400
+ {intl.formatMessage(messages.previewEmpty)}
401
+ </p>
402
+ )}
403
+ </aside>
404
+ </div>
405
+ </div>
406
+ );
407
+ };
408
+
409
+ export default NavigatorGuideContentView;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { Container } from 'semantic-ui-react';
3
+
4
+ const NavigatorGuideLayout = ({ bodyContent }) => (
5
+ <Container className="navigator-guide-search-layout">{bodyContent}</Container>
6
+ );
7
+
8
+ export default NavigatorGuideLayout;
@@ -7,6 +7,8 @@ export { default as ASTNavigation } from './theme/ASTNavigation/ASTNavigation';
7
7
  export { default as RedirectToLogin } from './theme/RedirectToLogin/RedirectToLogin';
8
8
  export { default as MissionSignatoryProfileView } from './theme/MissionSignatoryProfile/MissionSignatoryProfileView';
9
9
  export { default as AccordionList } from './theme/AccordionList/AccordionList';
10
+ export { default as CompareToolsView } from './theme/CompareTools/CompareToolsView';
11
+ export { CompareToolsPanel } from './theme/CompareTools/CompareToolsPanel';
10
12
 
11
13
  // Widgets
12
14
  export { default as RASTWidgetView } from './theme/Widgets/RASTWidgetView';
@@ -17,6 +17,7 @@ import {
17
17
  ACE_PROJECT,
18
18
  PUBLICATION_REPORT,
19
19
  TOOL,
20
+ EXTENDED_TOOL,
20
21
  VIDEO,
21
22
  C3S_INDICATOR,
22
23
  NEWS_ITEM,
@@ -89,6 +90,7 @@ describe('CollectionStatsView', () => {
89
90
  [ORGANISATION]: 22,
90
91
  [PUBLICATION_REPORT]: 187,
91
92
  [TOOL]: 14,
93
+ [EXTENDED_TOOL]: 10,
92
94
  [VIDEO]: 17,
93
95
  },
94
96
  },