@eeacms/volto-cca-policy 1.0.2 → 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.
- package/CHANGELOG.md +231 -16
- package/jest-addon.config.js +4 -2
- package/package.json +1 -1
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +228 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +167 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +189 -0
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueMapView.jsx +11 -0
- package/src/components/Search/NavigatorCatalogue/utils.js +101 -0
- package/src/components/Search/NavigatorCatalogue/utils.test.js +132 -0
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +409 -0
- package/src/components/Search/NavigatorGuide/NavigatorGuideLayout.jsx +8 -0
- package/src/components/index.js +2 -0
- package/src/components/theme/CompareTools/CompareToolsPanel.jsx +151 -0
- package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +96 -0
- package/src/components/theme/CompareTools/CompareToolsView.jsx +468 -0
- package/src/components/theme/CompareTools/utils.js +111 -0
- package/src/components/theme/CompareTools/utils.test.jsx +217 -0
- package/src/components/theme/Views/ExtendedToolView.jsx +201 -201
- package/src/components/theme/Views/ExtendedToolView.test.jsx +420 -58
- package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.js +1 -1
- package/src/helpers/Utils.jsx +29 -0
- package/src/helpers/index.js +3 -0
- package/src/index.js +28 -2
- package/src/search/index.js +6 -0
- package/src/search/navigator_catalogue/config.js +89 -0
- package/src/search/navigator_catalogue/facets.js +86 -0
- package/src/search/navigator_catalogue/views.js +28 -0
- package/src/search/navigator_guide/config.js +70 -0
- package/src/search/navigator_guide/facets.js +31 -0
- package/src/search/navigator_guide/guideSteps.js +93 -0
- package/src/slate-styles.less +4 -0
- package/src/state.js +1 -0
- package/theme/elements/button.overrides +0 -11
- package/theme/elements/label.overrides +15 -0
- package/theme/globals/blocks.less +3 -2
- package/theme/globals/navigator.less +967 -0
- package/theme/globals/site.overrides +5 -0
- package/theme/tokens/colors.less +2 -0
|
@@ -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;
|
package/src/components/index.js
CHANGED
|
@@ -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';
|
|
@@ -0,0 +1,151 @@
|
|
|
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 { Icon, Button } from 'semantic-ui-react';
|
|
6
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
7
|
+
import {
|
|
8
|
+
MAX_COMPARE_TOOLS,
|
|
9
|
+
compareToolsAtom,
|
|
10
|
+
getCompareLocation,
|
|
11
|
+
} from './utils';
|
|
12
|
+
|
|
13
|
+
const messages = defineMessages({
|
|
14
|
+
compareTools: {
|
|
15
|
+
id: 'Compare tools',
|
|
16
|
+
defaultMessage: 'Compare tools',
|
|
17
|
+
},
|
|
18
|
+
removeTool: {
|
|
19
|
+
id: 'Remove {title}',
|
|
20
|
+
defaultMessage: 'Remove {title}',
|
|
21
|
+
},
|
|
22
|
+
readyToCompare: {
|
|
23
|
+
id: 'Ready to compare',
|
|
24
|
+
defaultMessage: 'Ready to compare',
|
|
25
|
+
},
|
|
26
|
+
addAnotherTool: {
|
|
27
|
+
id: 'Add at least another tool',
|
|
28
|
+
defaultMessage: 'Add at least another tool',
|
|
29
|
+
},
|
|
30
|
+
compareSelectedTools: {
|
|
31
|
+
id: 'Compare selected tools',
|
|
32
|
+
defaultMessage: 'Compare selected tools',
|
|
33
|
+
},
|
|
34
|
+
addTool: {
|
|
35
|
+
id: 'Add a tool',
|
|
36
|
+
defaultMessage: 'Add a tool',
|
|
37
|
+
},
|
|
38
|
+
clearAll: {
|
|
39
|
+
id: 'Clear all',
|
|
40
|
+
defaultMessage: 'Clear all',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
|
|
45
|
+
const intl = useIntl();
|
|
46
|
+
const history = useHistory();
|
|
47
|
+
const currentLang = useSelector((state) => state.intl.locale);
|
|
48
|
+
const appConfig = suppliedAppConfig || {
|
|
49
|
+
landingPageURL: '/en/navigator/tool-catalogue',
|
|
50
|
+
};
|
|
51
|
+
const [selectedTools, setSelectedTools] = useAtom(compareToolsAtom);
|
|
52
|
+
const resolvableSelectedTools = selectedTools.filter((tool) => tool.uid);
|
|
53
|
+
const isReadyToCompare = resolvableSelectedTools.length >= 2;
|
|
54
|
+
const emptySlots = MAX_COMPARE_TOOLS - selectedTools.length;
|
|
55
|
+
|
|
56
|
+
if (selectedTools.length === 0) return null;
|
|
57
|
+
|
|
58
|
+
const removeTool = (toolUid) => {
|
|
59
|
+
setSelectedTools((tools) => tools.filter((tool) => tool.uid !== toolUid));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const compareTools = () => {
|
|
63
|
+
const returnURL = [
|
|
64
|
+
window.location.pathname,
|
|
65
|
+
window.location.search,
|
|
66
|
+
window.location.hash,
|
|
67
|
+
].join('');
|
|
68
|
+
|
|
69
|
+
history.push(
|
|
70
|
+
getCompareLocation(selectedTools, appConfig, returnURL, currentLang),
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="catalogue-compare-panel" aria-live="polite">
|
|
76
|
+
<div className="compare-panel-header">
|
|
77
|
+
<h2>
|
|
78
|
+
{intl.formatMessage(messages.compareTools)}
|
|
79
|
+
<span className="compare-panel-count">
|
|
80
|
+
{selectedTools.length}/{MAX_COMPARE_TOOLS}
|
|
81
|
+
</span>
|
|
82
|
+
</h2>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="compare-panel-content">
|
|
86
|
+
<div className="compare-panel-tools">
|
|
87
|
+
{selectedTools.map((tool) => (
|
|
88
|
+
<div key={tool.uid} className="compare-panel-tool">
|
|
89
|
+
<span className="navigator-tool-icon small" aria-hidden="true">
|
|
90
|
+
<Icon className="ri-file-line" />
|
|
91
|
+
</span>
|
|
92
|
+
<span className="compare-panel-tool-title">{tool.title}</span>
|
|
93
|
+
<Button
|
|
94
|
+
className="compare-panel-tool-clear"
|
|
95
|
+
aria-label={intl.formatMessage(messages.removeTool, {
|
|
96
|
+
title: tool.title,
|
|
97
|
+
})}
|
|
98
|
+
onClick={() => removeTool(tool.uid)}
|
|
99
|
+
>
|
|
100
|
+
<Icon className="ri-close-line" />
|
|
101
|
+
</Button>
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
{Array.from({ length: emptySlots }).map((_, index) => (
|
|
105
|
+
<div
|
|
106
|
+
key={`compare-placeholder-${index}`}
|
|
107
|
+
className="compare-panel-tool placeholder"
|
|
108
|
+
>
|
|
109
|
+
<span className="navigator-tool-icon small" aria-hidden="true">
|
|
110
|
+
<Icon className="ri-add-line" />
|
|
111
|
+
</span>
|
|
112
|
+
<span className="compare-panel-tool-title">
|
|
113
|
+
{intl.formatMessage(messages.addTool)}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className="compare-panel-actions">
|
|
120
|
+
<div
|
|
121
|
+
className={`compare-panel-status ${
|
|
122
|
+
isReadyToCompare ? 'ready' : 'incomplete'
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{isReadyToCompare && <Icon className="ri-check-line" />}
|
|
126
|
+
{intl.formatMessage(
|
|
127
|
+
isReadyToCompare
|
|
128
|
+
? messages.readyToCompare
|
|
129
|
+
: messages.addAnotherTool,
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
<Button
|
|
133
|
+
primary
|
|
134
|
+
className="compare-panel-submit"
|
|
135
|
+
disabled={!isReadyToCompare}
|
|
136
|
+
onClick={compareTools}
|
|
137
|
+
>
|
|
138
|
+
<Icon className="ri-layout-column-line" />
|
|
139
|
+
<span>{intl.formatMessage(messages.compareSelectedTools)}</span>
|
|
140
|
+
</Button>
|
|
141
|
+
<Button
|
|
142
|
+
className="compare-panel-clear-all"
|
|
143
|
+
onClick={() => setSelectedTools([])}
|
|
144
|
+
>
|
|
145
|
+
{intl.formatMessage(messages.clearAll)}
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
+
import { IntlProvider } from 'react-intl';
|
|
5
|
+
import { Provider as JotaiProvider } from 'jotai';
|
|
6
|
+
import { useHistory } from 'react-router-dom';
|
|
7
|
+
import { CompareToolsPanel } from './CompareToolsPanel';
|
|
8
|
+
|
|
9
|
+
jest.mock('react-redux', () => ({
|
|
10
|
+
...jest.requireActual('react-redux'),
|
|
11
|
+
useSelector: (selector) => selector({ intl: { locale: 'en' } }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('react-router-dom', () => ({
|
|
15
|
+
...jest.requireActual('react-router-dom'),
|
|
16
|
+
useHistory: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const renderPanel = (selectedTools, appConfig) => {
|
|
20
|
+
window.localStorage.setItem(
|
|
21
|
+
'cca-compare-tools',
|
|
22
|
+
JSON.stringify(selectedTools),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return render(
|
|
26
|
+
<IntlProvider locale="en">
|
|
27
|
+
<JotaiProvider>
|
|
28
|
+
<CompareToolsPanel appConfig={appConfig} />
|
|
29
|
+
</JotaiProvider>
|
|
30
|
+
</IntlProvider>,
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('CompareToolsPanel', () => {
|
|
35
|
+
const push = jest.fn();
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
jest.clearAllMocks();
|
|
39
|
+
window.localStorage.clear();
|
|
40
|
+
useHistory.mockReturnValue({ push });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not render without selected tools', () => {
|
|
44
|
+
const { container } = renderPanel([]);
|
|
45
|
+
|
|
46
|
+
expect(container).toBeEmptyDOMElement();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('shows an incomplete warning and removes the only tool', () => {
|
|
50
|
+
renderPanel([{ uid: 'one', title: 'Tool one' }]);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByText('Add at least another tool')).toHaveClass(
|
|
53
|
+
'compare-panel-status',
|
|
54
|
+
'incomplete',
|
|
55
|
+
);
|
|
56
|
+
expect(
|
|
57
|
+
screen.getByText('Compare selected tools').closest('button'),
|
|
58
|
+
).toBeDisabled();
|
|
59
|
+
|
|
60
|
+
fireEvent.click(screen.getByLabelText('Remove Tool one'));
|
|
61
|
+
expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('shows the ready state and opens the comparison page', () => {
|
|
65
|
+
renderPanel(
|
|
66
|
+
[
|
|
67
|
+
{ uid: 'one', title: 'Tool one' },
|
|
68
|
+
{ uid: 'two', title: 'Tool two' },
|
|
69
|
+
],
|
|
70
|
+
{ landingPageURL: '/en/navigator/tool-catalogue' },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText('Ready to compare')).toHaveClass(
|
|
74
|
+
'compare-panel-status',
|
|
75
|
+
'ready',
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
fireEvent.click(screen.getByText('Compare selected tools'));
|
|
79
|
+
|
|
80
|
+
expect(push).toHaveBeenCalledWith({
|
|
81
|
+
pathname: '/en/navigator/compare',
|
|
82
|
+
search: '?uid=one&uid=two',
|
|
83
|
+
state: { returnURL: '/' },
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('clears all selected tools', () => {
|
|
88
|
+
renderPanel([
|
|
89
|
+
{ uid: 'one', title: 'Tool one' },
|
|
90
|
+
{ uid: 'two', title: 'Tool two' },
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
fireEvent.click(screen.getByText('Clear all'));
|
|
94
|
+
expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
});
|