@eeacms/volto-eea-design-system 1.61.3 → 1.61.5

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,6 +4,23 @@ 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.61.5](https://github.com/eea/volto-eea-design-system/compare/1.61.4...1.61.5) - 6 October 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(subsite): use optional useAISearchIcon for subsite passed via headerSearch Controlpanel refs#308136 [nileshgulia1 - [`5c5bebc`](https://github.com/eea/volto-eea-design-system/commit/5c5bebcb47e99168553d3f811493ec7d98858c0e)]
12
+
13
+ #### :nail_care: Enhancements
14
+
15
+ - refactor: only turn off the site wide ai search action when subsite wants it. [nileshgulia1 - [`1cf5bed`](https://github.com/eea/volto-eea-design-system/commit/1cf5bed4bb4f5942635aabf2009f128171704a80)]
16
+
17
+ ### [1.61.4](https://github.com/eea/volto-eea-design-system/compare/1.61.3...1.61.4) - 6 October 2026
18
+
19
+ #### :rocket: New Features
20
+
21
+ - feat: add hideAction button if set in headerConfig refs#308136 [nileshgulia1 - [`03a1fc1`](https://github.com/eea/volto-eea-design-system/commit/03a1fc189347966660748ad9f6cad4efd2306a08)]
22
+ - feat: add support for queryParams if used with a volto search block #refs308136 [nileshgulia1 - [`99e25b9`](https://github.com/eea/volto-eea-design-system/commit/99e25b97b62b4969e73b95086fd8585a7aceb8c3)]
23
+
7
24
  ### [1.61.3](https://github.com/eea/volto-eea-design-system/compare/1.61.2...1.61.3) - 1 October 2026
8
25
 
9
26
  #### :hammer_and_wrench: Others
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-design-system",
3
- "version": "1.61.3",
3
+ "version": "1.61.5",
4
4
  "description": "@eeacms/volto-eea-design-system: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -17,7 +17,11 @@ import burgerIcon from '@eeacms/volto-eea-design-system/../theme/themes/eea/asse
17
17
  import HeaderSearchPopUp from './HeaderSearchPopUp';
18
18
  import HeaderMenuPopUp from './HeaderMenuPopUp';
19
19
  import { useAISummaryToggle } from './useAISummaryToggle';
20
- import { findBestMatchingMenuItem, isMenuItemActive } from './utils';
20
+ import {
21
+ findBestMatchingMenuItem,
22
+ getSearchInput,
23
+ isMenuItemActive,
24
+ } from './utils';
21
25
  import PropTypes from 'prop-types';
22
26
  import omit from 'lodash/omit';
23
27
  import { isInternalURL } from '@plone/volto/helpers/Url/Url';
@@ -162,10 +166,16 @@ const Main = ({
162
166
  const [burger, setBurger] = React.useState('');
163
167
  const searchInputRef = React.useRef(null);
164
168
  const [isClient, setIsClient] = React.useState();
165
- const useAISearchIcon = useSelector(
169
+ const siteUseAISearchIcon = useSelector(
166
170
  (state) =>
167
171
  state.reduxAsyncConnect?.headerSettings?.useAISearchIcon || false,
168
172
  );
173
+ // The search box matching the current path can turn the site-wide icon off
174
+ const useAISearchIcon =
175
+ siteUseAISearchIcon &&
176
+ (getSearchInput(headerSearchBox || [], { pathname: pathname || '' })
177
+ .useAISearchIcon ??
178
+ true);
169
179
 
170
180
  const [aiSummaryEnabled] = useAISummaryToggle();
171
181
 
@@ -772,12 +772,13 @@ test('menu click outside behavior with different active states', async () => {
772
772
  });
773
773
 
774
774
  describe('header search icon and AI summary state', () => {
775
- const renderMain = () =>
775
+ const renderMain = (props = {}) =>
776
776
  renderWithProvider(
777
777
  <Router history={createMemoryHistory()}>
778
778
  <Header>
779
779
  <Header.Main
780
780
  pathname="/test"
781
+ {...props}
781
782
  menuItems={[
782
783
  { '@id': '/test', url: '/test', title: 'Test', items: [] },
783
784
  ]}
@@ -824,4 +825,37 @@ describe('header search icon and AI summary state', () => {
824
825
  'search-line.svg',
825
826
  );
826
827
  });
828
+
829
+ describe('search box useAISearchIcon override', () => {
830
+ const headerSearchBox = [
831
+ { type: 'search-input', path: '/search' },
832
+ {
833
+ type: 'search-input',
834
+ matchpath: '/en/epanet',
835
+ path: '/en/epanet/site-search',
836
+ useAISearchIcon: false,
837
+ },
838
+ ];
839
+
840
+ it('shows the plain magnifier in the search box path that turns the AI icon off', () => {
841
+ mockState.reduxAsyncConnect.headerSettings.useAISearchIcon = true;
842
+
843
+ const { container } = renderMain({
844
+ pathname: '/en/epanet/about',
845
+ headerSearchBox,
846
+ });
847
+ expect(
848
+ container.querySelector('img[alt="Global search"]'),
849
+ ).toHaveAttribute('src', 'search-line.svg');
850
+ });
851
+
852
+ it('keeps the site-wide AI sparkle outside that path', () => {
853
+ mockState.reduxAsyncConnect.headerSettings.useAISearchIcon = true;
854
+
855
+ const { container } = renderMain({ pathname: '/en', headerSearchBox });
856
+ expect(
857
+ container.querySelector('img[alt="Global search"]'),
858
+ ).toHaveAttribute('src', 'ai-search.svg');
859
+ });
860
+ });
827
861
  });
@@ -10,6 +10,7 @@ import { handleEnterKeyPress } from '@eeacms/volto-eea-design-system/helpers';
10
10
  import searchSVG from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/search-line.svg';
11
11
  import aiSearchIcon from '@eeacms/volto-eea-design-system/../theme/themes/eea/assets/images/Header/ai-search.svg';
12
12
  import { useAISummaryToggle } from './useAISummaryToggle';
13
+ import { getSearchInput } from './utils';
13
14
 
14
15
  const getRandomItems = (arr, max) => {
15
16
  return (
@@ -19,18 +20,6 @@ const getRandomItems = (arr, max) => {
19
20
  );
20
21
  };
21
22
 
22
- const getSearchInput = (config, location) => {
23
- return (
24
- config.filter(
25
- (v) =>
26
- v.type === 'search-input' &&
27
- location.pathname.match(v.matchpath ? v.matchpath : v.path),
28
- )[0] ||
29
- config.filter((v) => v.type === 'search-input')[0] ||
30
- {}
31
- );
32
- };
33
-
34
23
  const serializeAction = (action, location) => {
35
24
  if (action.type === 'info' && isArray(action.content)) {
36
25
  const matchedContent =
@@ -75,13 +64,18 @@ function HeaderSearchPopUp({
75
64
  const nodeRef = React.useRef();
76
65
 
77
66
  const searchInput = getSearchInput(headerSearchBox, location);
78
- const actions =
79
- headerSearchBox
80
- .filter((v) => ['info', 'button-link'].includes(v.type))
81
- .map((v) => serializeAction(v, location)) || [];
67
+ // A search box with hideActions shows no info text or buttons below it
68
+ const actions = searchInput.hideActions
69
+ ? []
70
+ : headerSearchBox
71
+ .filter((v) => ['info', 'button-link'].includes(v.type))
72
+ .map((v) => serializeAction(v, location)) || [];
82
73
 
83
74
  const {
84
75
  path = '',
76
+ // URL parameter that carries the search text, e.g. 'SearchableText' for
77
+ // a page with Volto's Search block
78
+ queryParam = 'q',
85
79
  placeholder = 'Search',
86
80
  suggestionsTitle,
87
81
  maxToShow,
@@ -92,10 +86,13 @@ function HeaderSearchPopUp({
92
86
  getRandomItems(suggestions, maxToShow),
93
87
  );
94
88
 
95
- const useAISearchIcon = useSelector(
89
+ const siteUseAISearchIcon = useSelector(
96
90
  (state) =>
97
91
  state.reduxAsyncConnect?.headerSettings?.useAISearchIcon || false,
98
92
  );
93
+ // A search box can turn the site-wide icon off, e.g. a subsite search
94
+ const useAISearchIcon =
95
+ siteUseAISearchIcon && (searchInput.useAISearchIcon ?? true);
99
96
 
100
97
  const [aiSummaryEnabled] = useAISummaryToggle();
101
98
 
@@ -112,7 +109,7 @@ function HeaderSearchPopUp({
112
109
 
113
110
  const onSubmit = (event) => {
114
111
  const text = searchInputRef?.current?.inputRef?.current?.value;
115
- history.push(`${path}?q=${text}`);
112
+ history.push(`${path}?${queryParam}=${text}`);
116
113
 
117
114
  if (window?.searchContext?.resetSearch) {
118
115
  window.searchContext.resetSearch({ searchTerm: text });
@@ -168,7 +165,7 @@ function HeaderSearchPopUp({
168
165
  return (
169
166
  <List.Item key={i}>
170
167
  <Link
171
- to={`${path}?q=${item}`}
168
+ to={`${path}?${queryParam}=${item}`}
172
169
  onClick={() => onClickHandler(item)}
173
170
  >
174
171
  {item}
@@ -111,6 +111,48 @@ describe('HeaderSearchPopUp', () => {
111
111
  expect(history.location.pathname).toBe('/search');
112
112
  });
113
113
 
114
+ const submitText = (headerSearchBox, text) => {
115
+ const searchInputRef = React.createRef();
116
+ const { container } = render(
117
+ <Provider store={mockStore}>
118
+ <Router history={history}>
119
+ <HeaderSearchPopUp
120
+ headerSearchBox={headerSearchBox}
121
+ onClose={mockOnClose}
122
+ searchInputRef={searchInputRef}
123
+ triggerRefs={[]}
124
+ />
125
+ </Router>
126
+ </Provider>,
127
+ );
128
+ fireEvent.change(screen.getByPlaceholderText('Search'), {
129
+ target: { value: text },
130
+ });
131
+ fireEvent.submit(container.querySelector('form'));
132
+ };
133
+
134
+ it('should submit the search text as q by default', () => {
135
+ submitText(sampleHeaderSearchBox, 'plenary');
136
+ expect(history.location.pathname).toBe('/search');
137
+ expect(history.location.search).toBe('?q=plenary');
138
+ });
139
+
140
+ it('should submit the search text as the entry queryParam', () => {
141
+ submitText(
142
+ [
143
+ {
144
+ type: 'search-input',
145
+ path: '/en/epanet/site-search',
146
+ queryParam: 'SearchableText',
147
+ placeholder: 'Search',
148
+ },
149
+ ],
150
+ 'plenary',
151
+ );
152
+ expect(history.location.pathname).toBe('/en/epanet/site-search');
153
+ expect(history.location.search).toBe('?SearchableText=plenary');
154
+ });
155
+
114
156
  it('should navigate to the suggestion when a suggestion is clicked', () => {
115
157
  window.searchContext = { resetSearch: jest.fn() };
116
158
 
@@ -297,4 +339,62 @@ describe('HeaderSearchPopUp', () => {
297
339
  );
298
340
  });
299
341
  });
342
+
343
+ describe('search box useAISearchIcon override', () => {
344
+ beforeEach(() => {
345
+ window.localStorage.clear();
346
+ });
347
+
348
+ const renderWithEntry = (entry) =>
349
+ render(
350
+ <Provider store={mockStore}>
351
+ <Router history={history}>
352
+ <HeaderSearchPopUp
353
+ headerSearchBox={[{ ...sampleHeaderSearchBox[0], ...entry }]}
354
+ onClose={mockOnClose}
355
+ triggerRefs={[]}
356
+ />
357
+ </Router>
358
+ </Provider>,
359
+ );
360
+
361
+ it('shows the plain magnifier when the search box turns the AI icon off', () => {
362
+ mockState.reduxAsyncConnect.headerSettings.useAISearchIcon = true;
363
+
364
+ const { container } = renderWithEntry({ useAISearchIcon: false });
365
+ expect(container.querySelector('img')).toHaveAttribute(
366
+ 'src',
367
+ 'search-line.svg',
368
+ );
369
+ });
370
+ });
371
+
372
+ describe('hideActions', () => {
373
+ const renderWith = (headerSearchBox) =>
374
+ render(
375
+ <Provider store={mockStore}>
376
+ <Router history={history}>
377
+ <HeaderSearchPopUp
378
+ headerSearchBox={headerSearchBox}
379
+ onClose={mockOnClose}
380
+ triggerRefs={[]}
381
+ />
382
+ </Router>
383
+ </Provider>,
384
+ );
385
+
386
+ it('shows the actions by default', () => {
387
+ renderWith(sampleHeaderSearchBox);
388
+ expect(screen.getByText('Advanced Search')).toBeInTheDocument();
389
+ });
390
+
391
+ it('hides the actions when the matched search input has hideActions', () => {
392
+ renderWith([
393
+ { ...sampleHeaderSearchBox[0], hideActions: true },
394
+ ...sampleHeaderSearchBox.slice(1),
395
+ ]);
396
+ expect(screen.queryByText('Advanced Search')).toBeNull();
397
+ expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
398
+ });
399
+ });
300
400
  });
@@ -91,3 +91,24 @@ export const numbersToMenuItemColumns = (numbers) => {
91
91
  })
92
92
  .filter((col) => col !== '');
93
93
  };
94
+
95
+ /**
96
+ * Finds the search-input entry of the header search box config that matches
97
+ * the current path (its matchpath, else its path), falling back to the first
98
+ * search-input entry.
99
+ *
100
+ * @param {Array} config - headerSearchBox entries
101
+ * @param {Object} location - object with a pathname
102
+ * @returns {Object} The matched search-input entry, or {}
103
+ */
104
+ export const getSearchInput = (config, location) => {
105
+ return (
106
+ config.filter(
107
+ (v) =>
108
+ v.type === 'search-input' &&
109
+ location.pathname.match(v.matchpath ? v.matchpath : v.path),
110
+ )[0] ||
111
+ config.filter((v) => v.type === 'search-input')[0] ||
112
+ {}
113
+ );
114
+ };