@eeacms/volto-cca-policy 1.0.11 → 1.0.12
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 +16 -1
- package/package.json +1 -1
- package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +13 -3
- package/src/components/Search/NavigatorGuide/useGuideFacetOptions.js +27 -0
- package/src/components/Search/NavigatorGuide/useGuideFacetOptions.test.js +58 -0
- package/src/components/Search/NavigatorGuide/utils.js +30 -0
- package/src/components/Search/NavigatorGuide/utils.test.js +40 -0
- package/src/utils.test.js +127 -0
- package/theme/globals/navigator.less +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
### [1.0.12](https://github.com/eea/volto-cca-policy/compare/1.0.11...1.0.12) - 24 August 2026
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: disable unavailable navigator guide options [kreafox - [`4b114c2`](https://github.com/eea/volto-cca-policy/commit/4b114c2b0ea7a6a6528ae8ae378c2e671b640945)]
|
|
12
|
+
|
|
13
|
+
#### :nail_care: Enhancements
|
|
14
|
+
|
|
15
|
+
- change: add custom hook for guide facet options and add tests [kreafox - [`872eb9b`](https://github.com/eea/volto-cca-policy/commit/872eb9b35c41fc2094c605b113411448a8cef9a8)]
|
|
16
|
+
|
|
17
|
+
#### :hammer_and_wrench: Others
|
|
18
|
+
|
|
19
|
+
- test: increase utility coverage [kreafox - [`d9c3db8`](https://github.com/eea/volto-cca-policy/commit/d9c3db8cc6fb91ead00f83b822f6fe48c26a70b1)]
|
|
7
20
|
### [1.0.11](https://github.com/eea/volto-cca-policy/compare/1.0.10...1.0.11) - 21 August 2026
|
|
8
21
|
|
|
9
22
|
### [1.0.10](https://github.com/eea/volto-cca-policy/compare/1.0.9...1.0.10) - 19 August 2026
|
|
@@ -288,7 +301,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
288
301
|
#### :hammer_and_wrench: Others
|
|
289
302
|
|
|
290
303
|
- test: pin Chromium version 149 to work with Cypress [valentinab25 - [`659ec82`](https://github.com/eea/volto-cca-policy/commit/659ec823dd71fe592a3a7accda22effbe1fb07bc)]
|
|
291
|
-
## [1.0.0](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.
|
|
304
|
+
## [1.0.0](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.24...1.0.0) - 12 June 2026
|
|
305
|
+
|
|
306
|
+
### [1.0.0-alpha.24](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.23...1.0.0-alpha.24) - 24 August 2026
|
|
292
307
|
|
|
293
308
|
### [1.0.0-alpha.23](https://github.com/eea/volto-cca-policy/compare/1.0.0-alpha.22...1.0.0-alpha.23) - 19 August 2026
|
|
294
309
|
|
package/package.json
CHANGED
|
@@ -8,6 +8,8 @@ import URLManager from '@elastic/search-ui/lib/cjs/URLManager';
|
|
|
8
8
|
import { useSearchContext } from '@eeacms/search/lib/hocs';
|
|
9
9
|
import guideSteps from '../../../search/navigator_guide/guideSteps';
|
|
10
10
|
import { navigatorGuideStepAtom } from '../../../state';
|
|
11
|
+
import { mergeGuideOptions } from './utils';
|
|
12
|
+
import useGuideFacetOptions from './useGuideFacetOptions';
|
|
11
13
|
|
|
12
14
|
const messages = defineMessages({
|
|
13
15
|
noSteps: {
|
|
@@ -111,6 +113,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
111
113
|
totalResults,
|
|
112
114
|
} = searchContext;
|
|
113
115
|
const steps = guideSteps;
|
|
116
|
+
const allFacetOptions = useGuideFacetOptions(appConfig, steps);
|
|
114
117
|
const [storedActiveStep, setActiveStep] = useAtom(navigatorGuideStepAtom);
|
|
115
118
|
const activeStep =
|
|
116
119
|
Number.isInteger(storedActiveStep) &&
|
|
@@ -122,11 +125,16 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
122
125
|
const selectedValues =
|
|
123
126
|
(filters || []).find((filter) => filter.field === step?.field)?.values ||
|
|
124
127
|
[];
|
|
125
|
-
const options = getFacetOptions(facets, step?.field);
|
|
126
|
-
const isLastStep = activeStep === steps.length - 1;
|
|
127
128
|
const hasSelections = steps.some(({ field }) =>
|
|
128
129
|
isStepSelected(filters, field),
|
|
129
130
|
);
|
|
131
|
+
const options = mergeGuideOptions(
|
|
132
|
+
allFacetOptions?.[step?.field],
|
|
133
|
+
getFacetOptions(facets, step?.field),
|
|
134
|
+
selectedValues,
|
|
135
|
+
hasSelections,
|
|
136
|
+
);
|
|
137
|
+
const isLastStep = activeStep === steps.length - 1;
|
|
130
138
|
const selectedStepLabels = steps
|
|
131
139
|
.filter(({ field }) => isStepSelected(filters, field))
|
|
132
140
|
.map(({ label }) =>
|
|
@@ -262,10 +270,12 @@ const NavigatorGuideContentView = ({ appConfig }) => {
|
|
|
262
270
|
key={option.value}
|
|
263
271
|
className={`navigator-guide-option${
|
|
264
272
|
selectedValues.includes(option.value) ? ' selected' : ''
|
|
265
|
-
}`}
|
|
273
|
+
}${option.disabled ? ' disabled' : ''}`}
|
|
274
|
+
aria-disabled={option.disabled || undefined}
|
|
266
275
|
>
|
|
267
276
|
<Checkbox
|
|
268
277
|
checked={selectedValues.includes(option.value)}
|
|
278
|
+
disabled={option.disabled}
|
|
269
279
|
onChange={() => toggleValue(option.value)}
|
|
270
280
|
/>
|
|
271
281
|
<span>{option.value}</span>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getFacetOptions } from '@eeacms/search/components/SearchApp/useFacetsWithAllOptions';
|
|
3
|
+
|
|
4
|
+
const useGuideFacetOptions = (appConfig, steps) => {
|
|
5
|
+
const [allFacetOptions, setAllFacetOptions] = React.useState({});
|
|
6
|
+
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
let isCurrent = true;
|
|
9
|
+
const stepFields = steps.map(({ field }) => field);
|
|
10
|
+
|
|
11
|
+
getFacetOptions(appConfig, stepFields)
|
|
12
|
+
.then((facetOptions) => {
|
|
13
|
+
if (isCurrent) setAllFacetOptions(facetOptions);
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {
|
|
16
|
+
// Live facet options remain available if the supplementary request fails.
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
isCurrent = false;
|
|
21
|
+
};
|
|
22
|
+
}, [appConfig, steps]);
|
|
23
|
+
|
|
24
|
+
return allFacetOptions;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default useGuideFacetOptions;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks';
|
|
2
|
+
import { getFacetOptions } from '@eeacms/search/components/SearchApp/useFacetsWithAllOptions';
|
|
3
|
+
import useGuideFacetOptions from './useGuideFacetOptions';
|
|
4
|
+
|
|
5
|
+
jest.mock(
|
|
6
|
+
'@eeacms/search/components/SearchApp/useFacetsWithAllOptions',
|
|
7
|
+
() => ({ getFacetOptions: jest.fn() }),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const flushPromises = () => act(() => Promise.resolve());
|
|
11
|
+
|
|
12
|
+
describe('useGuideFacetOptions', () => {
|
|
13
|
+
const appConfig = { facets: [] };
|
|
14
|
+
const steps = [{ field: 'sector' }, { field: 'stage' }];
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getFacetOptions.mockReset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('loads the complete option list for every guide field', async () => {
|
|
21
|
+
const facetOptions = { sector: ['Energy'], stage: ['Step 1'] };
|
|
22
|
+
getFacetOptions.mockResolvedValue(facetOptions);
|
|
23
|
+
|
|
24
|
+
const { result } = renderHook(() => useGuideFacetOptions(appConfig, steps));
|
|
25
|
+
await flushPromises();
|
|
26
|
+
|
|
27
|
+
expect(getFacetOptions).toHaveBeenCalledWith(appConfig, [
|
|
28
|
+
'sector',
|
|
29
|
+
'stage',
|
|
30
|
+
]);
|
|
31
|
+
expect(result.current).toEqual(facetOptions);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps the fallback state when loading fails', async () => {
|
|
35
|
+
getFacetOptions.mockRejectedValue(new Error('Search unavailable'));
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(() => useGuideFacetOptions(appConfig, steps));
|
|
38
|
+
await flushPromises();
|
|
39
|
+
|
|
40
|
+
expect(result.current).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not update state after unmounting', async () => {
|
|
44
|
+
let resolveRequest;
|
|
45
|
+
getFacetOptions.mockReturnValue(
|
|
46
|
+
new Promise((resolve) => {
|
|
47
|
+
resolveRequest = resolve;
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { unmount } = renderHook(() =>
|
|
52
|
+
useGuideFacetOptions(appConfig, steps),
|
|
53
|
+
);
|
|
54
|
+
unmount();
|
|
55
|
+
resolveRequest({ sector: ['Energy'] });
|
|
56
|
+
await flushPromises();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const mergeGuideOptions = (
|
|
2
|
+
availableValues = [],
|
|
3
|
+
facetOptions = [],
|
|
4
|
+
selectedValues = [],
|
|
5
|
+
hasGuideSelections = false,
|
|
6
|
+
) => {
|
|
7
|
+
const optionsByValue = new Map(
|
|
8
|
+
facetOptions.map(({ value, count }) => [value, { value, count }]),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
availableValues.forEach((value) => {
|
|
12
|
+
if (!optionsByValue.has(value)) {
|
|
13
|
+
optionsByValue.set(value, { value, count: 0 });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
selectedValues.forEach((value) => {
|
|
18
|
+
if (!optionsByValue.has(value)) {
|
|
19
|
+
optionsByValue.set(value, { value, count: 0 });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return Array.from(optionsByValue.values()).map((option) => ({
|
|
24
|
+
...option,
|
|
25
|
+
disabled:
|
|
26
|
+
hasGuideSelections &&
|
|
27
|
+
option.count === 0 &&
|
|
28
|
+
!selectedValues.includes(option.value),
|
|
29
|
+
}));
|
|
30
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mergeGuideOptions } from './utils';
|
|
2
|
+
|
|
3
|
+
describe('Navigator Guide utilities', () => {
|
|
4
|
+
it('keeps unavailable values and marks them as disabled', () => {
|
|
5
|
+
expect(
|
|
6
|
+
mergeGuideOptions(
|
|
7
|
+
['Step 1', 'Step 2', 'Step 6'],
|
|
8
|
+
[
|
|
9
|
+
{ value: 'Step 1', count: 4 },
|
|
10
|
+
{ value: 'Step 2', count: 2 },
|
|
11
|
+
],
|
|
12
|
+
[],
|
|
13
|
+
true,
|
|
14
|
+
),
|
|
15
|
+
).toEqual([
|
|
16
|
+
{ value: 'Step 1', count: 4, disabled: false },
|
|
17
|
+
{ value: 'Step 2', count: 2, disabled: false },
|
|
18
|
+
{ value: 'Step 6', count: 0, disabled: true },
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps a selected zero-count value enabled so it can be removed', () => {
|
|
23
|
+
expect(mergeGuideOptions([], [], ['Step 6'], true)).toEqual([
|
|
24
|
+
{ value: 'Step 6', count: 0, disabled: false },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps every value enabled before the guide has been refined', () => {
|
|
29
|
+
expect(mergeGuideOptions(['Step 1', 'Step 6'], [], [])).toEqual([
|
|
30
|
+
{ value: 'Step 1', count: 0, disabled: false },
|
|
31
|
+
{ value: 'Step 6', count: 0, disabled: false },
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('falls back to live facet options while the full list is loading', () => {
|
|
36
|
+
expect(mergeGuideOptions([], [{ value: 'Energy', count: 3 }], [])).toEqual([
|
|
37
|
+
{ value: 'Energy', count: 3, disabled: false },
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
2
|
+
import {
|
|
3
|
+
blockAvailableInMission,
|
|
4
|
+
extractPlanNameAndURL,
|
|
5
|
+
filterBlocks,
|
|
6
|
+
formatTextToHTML,
|
|
7
|
+
getBaseUrl,
|
|
8
|
+
getFilteredBlocks,
|
|
9
|
+
hasTypeOfBlock,
|
|
10
|
+
isEmpty,
|
|
11
|
+
normalizeImageFileName,
|
|
12
|
+
} from './utils';
|
|
13
|
+
|
|
14
|
+
jest.mock('@plone/volto/helpers/Url/Url', () => ({
|
|
15
|
+
flattenToAppURL: jest.fn((url) => url.replace('https://example.com', '')),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('general utilities', () => {
|
|
19
|
+
it('checks whether blocks are available inside and outside Mission', () => {
|
|
20
|
+
expect(blockAvailableInMission({}, { id: 'text' })).toBe(false);
|
|
21
|
+
expect(
|
|
22
|
+
blockAvailableInMission(
|
|
23
|
+
{ '@id': 'https://example.com/en/mission/page' },
|
|
24
|
+
{ id: 'mkh_map' },
|
|
25
|
+
),
|
|
26
|
+
).toBe(false);
|
|
27
|
+
expect(
|
|
28
|
+
blockAvailableInMission(
|
|
29
|
+
{ '@id': 'https://example.com/en/mission/page' },
|
|
30
|
+
{ id: 'text' },
|
|
31
|
+
),
|
|
32
|
+
).toBe(true);
|
|
33
|
+
expect(
|
|
34
|
+
blockAvailableInMission(
|
|
35
|
+
{ '@id': 'https://example.com/en/page' },
|
|
36
|
+
{ id: 'rastBlock' },
|
|
37
|
+
),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('resolves a block base URL from the supported properties', () => {
|
|
42
|
+
expect(
|
|
43
|
+
getBaseUrl({ data: { href: [{ '@id': 'https://example.com/page' }] } }),
|
|
44
|
+
).toBe('/page');
|
|
45
|
+
expect(getBaseUrl({ path: '/from-path' })).toBe('/from-path');
|
|
46
|
+
expect(getBaseUrl({ location: { pathname: '/from-location' } })).toBe(
|
|
47
|
+
'/from-location',
|
|
48
|
+
);
|
|
49
|
+
expect(getBaseUrl({})).toBe('');
|
|
50
|
+
expect(flattenToAppURL).toHaveBeenCalledTimes(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('finds nested block types and removes selected block types', () => {
|
|
54
|
+
expect(
|
|
55
|
+
hasTypeOfBlock({ blocks: { child: { '@type': 'listing' } } }, 'listing'),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
expect(hasTypeOfBlock({ child: null }, 'listing')).toBe(false);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
filterBlocks(
|
|
61
|
+
{
|
|
62
|
+
blocks: {
|
|
63
|
+
one: { '@type': 'text' },
|
|
64
|
+
two: { '@type': 'listing' },
|
|
65
|
+
},
|
|
66
|
+
blocks_layout: { items: ['one', 'two'] },
|
|
67
|
+
},
|
|
68
|
+
['listing'],
|
|
69
|
+
),
|
|
70
|
+
).toEqual({
|
|
71
|
+
blocks: { one: { '@type': 'text' } },
|
|
72
|
+
blocks_layout: { items: ['one'] },
|
|
73
|
+
hasBlockTypes: true,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('formats escaped text, paragraphs, bullets, and links as HTML', () => {
|
|
78
|
+
expect(formatTextToHTML()).toBe('');
|
|
79
|
+
expect(formatTextToHTML('Simple text')).toBe('<p>Simple text</p>');
|
|
80
|
+
expect(formatTextToHTML('First\\n\\nSecond')).toBe('First</p><p>Second');
|
|
81
|
+
expect(formatTextToHTML('Item\\no next')).toBe('<p>Item<br />• next</p>');
|
|
82
|
+
expect(formatTextToHTML('See https://example.com')).toContain(
|
|
83
|
+
'<a href="https://example.com"',
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps only parent blocks without the excluded nested type', () => {
|
|
88
|
+
const result = getFilteredBlocks(
|
|
89
|
+
{
|
|
90
|
+
blocks: {
|
|
91
|
+
keep: { '@type': 'group', blocks: { child: { '@type': 'text' } } },
|
|
92
|
+
exclude: {
|
|
93
|
+
'@type': 'group',
|
|
94
|
+
data: { blocks: { child: { '@type': 'map' } } },
|
|
95
|
+
},
|
|
96
|
+
other: { '@type': 'text' },
|
|
97
|
+
},
|
|
98
|
+
blocks_layout: { items: ['keep', 'exclude', 'other'] },
|
|
99
|
+
},
|
|
100
|
+
'group',
|
|
101
|
+
'map',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(result.keptKeys).toEqual(['keep']);
|
|
105
|
+
expect(result.blocks_layout.items).toEqual(['keep']);
|
|
106
|
+
expect(result.hasMatches).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('extracts plan links and normalizes common values', () => {
|
|
110
|
+
expect(extractPlanNameAndURL()).toEqual({ name: '', url: '' });
|
|
111
|
+
expect(
|
|
112
|
+
extractPlanNameAndURL('Adaptation plan (https://example.com/plan)'),
|
|
113
|
+
).toEqual({ name: 'Adaptation plan', url: 'https://example.com/plan' });
|
|
114
|
+
expect(
|
|
115
|
+
extractPlanNameAndURL(
|
|
116
|
+
'Plan https://example.com/source https://example.com/document',
|
|
117
|
+
),
|
|
118
|
+
).toEqual({ name: 'Plan', url: 'https://example.com/document' });
|
|
119
|
+
|
|
120
|
+
expect(isEmpty()).toBe(true);
|
|
121
|
+
expect(isEmpty([])).toBe(true);
|
|
122
|
+
expect(isEmpty(['value'])).toBe(false);
|
|
123
|
+
expect(normalizeImageFileName()).toBe('');
|
|
124
|
+
expect(normalizeImageFileName('map(final).png')).toBe('map-final.png');
|
|
125
|
+
expect(normalizeImageFileName('map(final)')).toBe('map-final-');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -1004,6 +1004,12 @@ body.searchlib-page .searchapp-navigatorCatalogueSearch {
|
|
|
1004
1004
|
background: fade(@primaryColor, 5%);
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
&.disabled {
|
|
1008
|
+
background: #f5f5f5;
|
|
1009
|
+
color: @navigatorInactiveColor;
|
|
1010
|
+
opacity: 0.65;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1007
1013
|
> span {
|
|
1008
1014
|
flex: 1;
|
|
1009
1015
|
}
|