@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.
- package/.github/workflows/betterleaks.yml +133 -0
- package/.gitleaks.toml +89 -0
- package/CHANGELOG.md +262 -1
- package/README.md +59 -0
- package/RELEASE.md +1 -1
- package/jest-addon.config.js +7 -3
- package/package.json +3 -2
- 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/manage/Blocks/CollectionStatistics/CollectionStatsView.test.jsx +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 +409 -0
- package/src/components/theme/Views/ExtendedToolView.test.jsx +436 -0
- package/src/constants.js +1 -0
- package/src/customizations/@plone-collective/volto-rss-provider/express-middleware.js +1 -1
- package/src/customizations/volto/components/theme/View/LinkView.jsx +1 -1
- package/src/customizations/volto/reducers/breadcrumbs/breadcrumbs.js +1 -1
- package/src/customizations/volto/server.jsx +1 -1
- package/src/helpers/Utils.jsx +29 -0
- package/src/helpers/index.js +3 -0
- package/src/index.js +31 -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,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;
|
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';
|
|
@@ -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
|
},
|