@eeacms/volto-cca-policy 1.0.34 → 1.0.35

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 CHANGED
@@ -4,7 +4,20 @@ 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.34](https://github.com/eea/volto-cca-policy/compare/1.0.33...1.0.34) - 5 October 2026
7
+ ### [1.0.35](https://github.com/eea/volto-cca-policy/compare/1.0.34...1.0.35) - 6 October 2026
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix(test): add waitFor to image load assertions [kreafox - [`84eed2e`](https://github.com/eea/volto-cca-policy/commit/84eed2e15a911d9a8a4a1d20e7d29b3de261bb7d)]
12
+
13
+ #### :nail_care: Enhancements
14
+
15
+ - change(navigator): conditionally render result count based on selections [kreafox - [`0ed5449`](https://github.com/eea/volto-cca-policy/commit/0ed54490c16c9a5b4652fd1de47adbad8d2c8256)]
16
+ - change(NavigatorGuide): implement sorting for guide options - refs #304612 [kreafox - [`4ffcbff`](https://github.com/eea/volto-cca-policy/commit/4ffcbff4f98a2c95b1c88dfc0f662c2906678c13)]
17
+ - change(navigator): implement default sorting in guide me tools listing - refs #304612 [kreafox - [`a9f7fb3`](https://github.com/eea/volto-cca-policy/commit/a9f7fb31b276ed2c28d0fa24b800bebfd0ff5425)]
18
+ - change(CatalogueChatView): remove CompareToolsPanel and update tests - refs #309337 [kreafox - [`a5bb420`](https://github.com/eea/volto-cca-policy/commit/a5bb420e8a3229b1a04fd38f36ee87266bb0e59d)]
19
+
20
+ ### [1.0.34](https://github.com/eea/volto-cca-policy/compare/1.0.33...1.0.34) - 6 October 2026
8
21
 
9
22
  #### :nail_care: Enhancements
10
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -5,11 +5,11 @@ import { useHistory } from 'react-router-dom';
5
5
  import { defineMessages, useIntl } from 'react-intl';
6
6
  import { Button, Checkbox, Icon, Loader, Message } from 'semantic-ui-react';
7
7
  import URLManager from '@elastic/search-ui/lib/cjs/URLManager';
8
- import { useSearchContext } from '@eeacms/search/lib/hocs';
8
+ import { useSearchContext, useSearchDriver } from '@eeacms/search/lib/hocs';
9
9
  import ToolThumbnail from '@eeacms/volto-cca-policy/components/theme/ToolThumbnail/ToolThumbnail';
10
10
  import guideSteps from '../../../search/navigator_guide/guideSteps';
11
11
  import { navigatorGuideStepAtom } from '../../../state';
12
- import { mergeGuideOptions, sortAdaptationSteps } from './utils';
12
+ import { mergeGuideOptions, sortGuideOptions } from './utils';
13
13
  import { rawValueAsArray } from '../NavigatorCatalogue/utils';
14
14
  import useGuideFacetOptions from './useGuideFacetOptions';
15
15
  import { getNavigatorCataloguePageURL } from '../../Search/NavigatorCatalogue/utils';
@@ -106,6 +106,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
106
106
  const history = useHistory();
107
107
  const currentLang = useSelector((state) => state.intl.locale || 'en');
108
108
  const searchContext = useSearchContext();
109
+ const driver = useSearchDriver();
109
110
  const {
110
111
  addFilter,
111
112
  facets,
@@ -113,8 +114,12 @@ const NavigatorGuideContentView = ({ appConfig }) => {
113
114
  isLoading,
114
115
  removeFilter,
115
116
  results,
117
+ searchTerm,
118
+ sortDirection,
119
+ sortField,
116
120
  totalResults,
117
121
  } = searchContext;
122
+ const defaultSortApplied = React.useRef(false);
118
123
  const steps = guideSteps;
119
124
  const allFacetOptions = useGuideFacetOptions(appConfig, steps);
120
125
  const [storedActiveStep, setActiveStep] = useAtom(navigatorGuideStepAtom);
@@ -137,10 +142,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
137
142
  selectedValues,
138
143
  hasSelections,
139
144
  );
140
- const options =
141
- step?.id === 'adaptationStage'
142
- ? sortAdaptationSteps(mergedOptions)
143
- : mergedOptions;
145
+ const options = sortGuideOptions(mergedOptions, step?.id, currentLang);
144
146
  const isLastStep = activeStep === steps.length - 1;
145
147
  const selectedStepLabels = steps
146
148
  .filter(({ field }) => isStepSelected(filters, field))
@@ -171,6 +173,23 @@ const NavigatorGuideContentView = ({ appConfig }) => {
171
173
  }
172
174
  }, [activeStep, setActiveStep, storedActiveStep]);
173
175
 
176
+ React.useEffect(() => {
177
+ if (!driver || defaultSortApplied.current) return;
178
+ defaultSortApplied.current = true;
179
+
180
+ if (
181
+ searchTerm?.trim() ||
182
+ sortField ||
183
+ sortDirection ||
184
+ !appConfig.defaultSort
185
+ ) {
186
+ return;
187
+ }
188
+
189
+ const [field, direction] = appConfig.defaultSort.split('|');
190
+ if (field && direction) driver.setSort(field, direction);
191
+ }, [appConfig.defaultSort, driver, searchTerm, sortDirection, sortField]);
192
+
174
193
  const toggleValue = (value) => {
175
194
  if (selectedValues.includes(value)) {
176
195
  removeFilter(step.field, value, step.filterType);
@@ -339,16 +358,18 @@ const NavigatorGuideContentView = ({ appConfig }) => {
339
358
  {intl.formatMessage(messages.livePreview)}
340
359
  </div>
341
360
  </div>
342
- <div className="navigator-guide-result-count">
343
- <span className="navigator-guide-result-count-value">
344
- {isLoading ? '…' : totalResults || 0}
345
- </span>
346
- <p>
347
- {intl.formatMessage(messages.toolsMatch, {
348
- count: totalResults || 0,
349
- })}
350
- </p>
351
- </div>
361
+ {hasSelections && (
362
+ <div className="navigator-guide-result-count">
363
+ <span className="navigator-guide-result-count-value">
364
+ {isLoading ? '…' : totalResults || 0}
365
+ </span>
366
+ <p>
367
+ {intl.formatMessage(messages.toolsMatch, {
368
+ count: totalResults || 0,
369
+ })}
370
+ </p>
371
+ </div>
372
+ )}
352
373
  {selectedStepLabels.length > 0 && (
353
374
  <p className="navigator-guide-preview-refinements">
354
375
  {intl.formatMessage(messages.refinedBy, {
@@ -5,7 +5,7 @@ import { IntlProvider } from 'react-intl';
5
5
  import { useAtom } from 'jotai';
6
6
  import { useSelector } from 'react-redux';
7
7
  import { useHistory } from 'react-router-dom';
8
- import { useSearchContext } from '@eeacms/search/lib/hocs';
8
+ import { useSearchContext, useSearchDriver } from '@eeacms/search/lib/hocs';
9
9
  import NavigatorGuideContentView from './NavigatorGuideContentView';
10
10
  import useGuideFacetOptions from './useGuideFacetOptions';
11
11
 
@@ -15,7 +15,10 @@ jest.mock('jotai', () => ({
15
15
  }));
16
16
  jest.mock('react-redux', () => ({ useSelector: jest.fn() }));
17
17
  jest.mock('react-router-dom', () => ({ useHistory: jest.fn() }));
18
- jest.mock('@eeacms/search/lib/hocs', () => ({ useSearchContext: jest.fn() }));
18
+ jest.mock('@eeacms/search/lib/hocs', () => ({
19
+ useSearchContext: jest.fn(),
20
+ useSearchDriver: jest.fn(),
21
+ }));
19
22
  jest.mock('./useGuideFacetOptions', () => ({
20
23
  __esModule: true,
21
24
  default: jest.fn(),
@@ -32,11 +35,20 @@ const renderGuide = (result, searchContext = {}) => {
32
35
  });
33
36
  return render(
34
37
  <IntlProvider locale="en">
35
- <NavigatorGuideContentView appConfig={{ previewResultsLimit: 3 }} />
38
+ <NavigatorGuideContentView
39
+ appConfig={{
40
+ previewResultsLimit: 3,
41
+ defaultSort: 'title.index|asc',
42
+ }}
43
+ />
36
44
  </IntlProvider>,
37
45
  );
38
46
  };
39
47
 
48
+ beforeEach(() => {
49
+ useSearchDriver.mockReturnValue(undefined);
50
+ });
51
+
40
52
  describe('NavigatorGuideContentView thumbnails', () => {
41
53
  beforeEach(() => {
42
54
  jest.clearAllMocks();
@@ -94,6 +106,64 @@ describe('NavigatorGuideContentView thumbnails', () => {
94
106
  });
95
107
  });
96
108
 
109
+ describe('NavigatorGuideContentView empty preview', () => {
110
+ beforeEach(() => {
111
+ jest.clearAllMocks();
112
+ useAtom.mockReturnValue([0, jest.fn()]);
113
+ useSelector.mockReturnValue('en');
114
+ useHistory.mockReturnValue({ push: jest.fn() });
115
+ useGuideFacetOptions.mockReturnValue({});
116
+ });
117
+
118
+ it('hides the result count while the preview empty state is visible', () => {
119
+ const { container } = renderGuide(
120
+ { title: 'Guide tool', href: '/tools/guide-tool' },
121
+ { filters: [] },
122
+ );
123
+
124
+ expect(
125
+ container.querySelector('.navigator-guide-preview-empty'),
126
+ ).toBeInTheDocument();
127
+ expect(
128
+ container.querySelector('.navigator-guide-result-count'),
129
+ ).not.toBeInTheDocument();
130
+ });
131
+ });
132
+
133
+ describe('NavigatorGuideContentView sorting', () => {
134
+ beforeEach(() => {
135
+ jest.clearAllMocks();
136
+ useAtom.mockReturnValue([0, jest.fn()]);
137
+ useSelector.mockReturnValue('en');
138
+ useHistory.mockReturnValue({ push: jest.fn() });
139
+ useGuideFacetOptions.mockReturnValue({});
140
+ });
141
+
142
+ it('starts with title A-Z when URL filters leave the sort unset', () => {
143
+ const driver = { setSort: jest.fn() };
144
+ useSearchDriver.mockReturnValue(driver);
145
+
146
+ renderGuide(
147
+ { title: 'Guide tool', href: '/tools/guide-tool' },
148
+ { searchTerm: '', sortField: '', sortDirection: '' },
149
+ );
150
+
151
+ expect(driver.setSort).toHaveBeenCalledWith('title.index', 'asc');
152
+ });
153
+
154
+ it('preserves an active sort', () => {
155
+ const driver = { setSort: jest.fn() };
156
+ useSearchDriver.mockReturnValue(driver);
157
+
158
+ renderGuide(
159
+ { title: 'Guide tool', href: '/tools/guide-tool' },
160
+ { sortField: 'title.index', sortDirection: 'desc' },
161
+ );
162
+
163
+ expect(driver.setSort).not.toHaveBeenCalled();
164
+ });
165
+ });
166
+
97
167
  describe('NavigatorGuideContentView adaptation steps', () => {
98
168
  beforeEach(() => {
99
169
  jest.clearAllMocks();
@@ -43,3 +43,10 @@ export const sortAdaptationSteps = (options) =>
43
43
  if (bStep) return 1;
44
44
  return a.value.localeCompare(b.value);
45
45
  });
46
+
47
+ export const sortGuideOptions = (options, stepId, locale = 'en') =>
48
+ stepId === 'adaptationStage'
49
+ ? sortAdaptationSteps(options)
50
+ : [...options].sort((a, b) =>
51
+ a.value.localeCompare(b.value, locale, { sensitivity: 'base' }),
52
+ );
@@ -1,4 +1,8 @@
1
- import { mergeGuideOptions, sortAdaptationSteps } from './utils';
1
+ import {
2
+ mergeGuideOptions,
3
+ sortAdaptationSteps,
4
+ sortGuideOptions,
5
+ } from './utils';
2
6
 
3
7
  describe('Navigator Guide utilities', () => {
4
8
  it('keeps unavailable values and marks them as disabled', () => {
@@ -54,4 +58,29 @@ describe('Navigator Guide utilities', () => {
54
58
  ]);
55
59
  expect(options[0].value).toBe('Step 10: Review');
56
60
  });
61
+
62
+ it('sorts regular guide options alphabetically without mutating them', () => {
63
+ const options = [
64
+ { value: 'Water', count: 3 },
65
+ { value: 'Agriculture', count: 8 },
66
+ { value: 'Biodiversity', count: 5 },
67
+ ];
68
+
69
+ expect(
70
+ sortGuideOptions(options, 'adaptationSectors').map(({ value }) => value),
71
+ ).toEqual(['Agriculture', 'Biodiversity', 'Water']);
72
+ expect(options[0].value).toBe('Water');
73
+ });
74
+
75
+ it('uses numeric cycle-step order for the adaptation stage', () => {
76
+ const options = [
77
+ { value: 'Step 10: Review', count: 3 },
78
+ { value: 'Step 2: Assess', count: 8 },
79
+ { value: 'Step 1: Prepare', count: 5 },
80
+ ];
81
+
82
+ expect(
83
+ sortGuideOptions(options, 'adaptationStage').map(({ value }) => value),
84
+ ).toEqual(['Step 1: Prepare', 'Step 2: Assess', 'Step 10: Review']);
85
+ });
57
86
  });
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import { ChatWindow } from '@eeacms/volto-eea-chatbot/ChatBlock/chat';
3
- import { CompareToolsPanel } from '../../../theme/CompareTools/CompareToolsPanel';
4
3
 
5
4
  import { CcaDocCard } from './DocumentCard';
6
5
  import { remarkCcaDocCards } from './docCards';
@@ -33,24 +32,17 @@ const extraRemarkPlugins = [remarkCcaDocCards, remarkCcaNextSteps];
33
32
  * turned into inline Navigator-style catalogue cards (`CcaDocCard`),
34
33
  * and "Suggested next steps" sections are turned into styled callout boxes.
35
34
  *
36
- * It also renders `<CompareToolsPanel />` so that when users activate the
37
- * "Compare" checkbox on any document card, the floating comparison panel
38
- * appears and allows navigating to the side-by-side comparison view.
39
- *
40
35
  * `ChatBlockView` renders this as `<Presentation persona {...blockData} />`,
41
36
  * so all block fields arrive as top-level props and are forwarded unchanged.
42
37
  */
43
38
  function CatalogueChatView(props) {
44
39
  return (
45
- <>
46
- <ChatWindow
47
- {...props}
48
- hideSourcesTab
49
- extraRemarkPlugins={extraRemarkPlugins}
50
- extraMarkdownComponents={extraMarkdownComponents}
51
- />
52
- <CompareToolsPanel />
53
- </>
40
+ <ChatWindow
41
+ {...props}
42
+ hideSourcesTab
43
+ extraRemarkPlugins={extraRemarkPlugins}
44
+ extraMarkdownComponents={extraMarkdownComponents}
45
+ />
54
46
  );
55
47
  }
56
48
 
@@ -17,10 +17,6 @@ jest.mock('@eeacms/volto-eea-chatbot/ChatBlock/chat', () => ({
17
17
  )),
18
18
  }));
19
19
 
20
- jest.mock('../../../theme/CompareTools/CompareToolsPanel', () => ({
21
- CompareToolsPanel: jest.fn(() => <div data-testid="compare-tools-panel" />),
22
- }));
23
-
24
20
  describe('CatalogueChatView', () => {
25
21
  beforeEach(() => {
26
22
  ChatWindow.mockClear();
@@ -74,9 +70,4 @@ describe('CatalogueChatView', () => {
74
70
  expect(secondMd).toBe(firstMd);
75
71
  expect(secondPlugins).toBe(firstPlugins);
76
72
  });
77
-
78
- it('renders the CompareToolsPanel alongside the chat window', () => {
79
- const { getByTestId } = render(<CatalogueChatView persona={{}} />);
80
- expect(getByTestId('compare-tools-panel')).toBeInTheDocument();
81
- });
82
73
  });
@@ -1,6 +1,12 @@
1
1
  import React from 'react';
2
2
  import '@testing-library/jest-dom';
3
- import { fireEvent, render, screen, within } from '@testing-library/react';
3
+ import {
4
+ fireEvent,
5
+ render,
6
+ screen,
7
+ waitFor,
8
+ within,
9
+ } from '@testing-library/react';
4
10
  import { IntlProvider } from 'react-intl';
5
11
  import { useAtom } from 'jotai';
6
12
  import { useDispatch, useSelector } from 'react-redux';
@@ -210,8 +216,10 @@ describe('CompareToolsView', () => {
210
216
 
211
217
  fireEvent.load(img);
212
218
 
213
- expect(img.style.display).toBe('');
214
- expect(thumbnail.querySelector('.ri-file-line')).not.toBeInTheDocument();
219
+ await waitFor(() => {
220
+ expect(img.style.display).toBe('');
221
+ expect(thumbnail.querySelector('.ri-file-line')).not.toBeInTheDocument();
222
+ });
215
223
  });
216
224
 
217
225
  it('falls back to the file icon when the image fails', async () => {
@@ -8,6 +8,7 @@ const previewResultsLimit = 3;
8
8
 
9
9
  const navigatorGuideConfig = {
10
10
  title: 'Navigator Guide',
11
+ defaultSort: 'title.index|asc',
11
12
  alwaysSearchOnInitialLoad: true,
12
13
  resultsPerPage: previewResultsLimit,
13
14
  previewResultsLimit,