@eeacms/volto-clms-theme 1.1.289 → 1.1.291

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,18 @@ 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.1.291](https://github.com/eea/volto-clms-theme/compare/1.1.290...1.1.291) - 22 May 2026
8
+
9
+ ### [1.1.290](https://github.com/eea/volto-clms-theme/compare/1.1.289...1.1.290) - 21 May 2026
10
+
11
+ #### :rocket: New Features
12
+
13
+ - feat: unify the design of technical library search with global search and dataset catalogue -refs #291483 [ana-oprea - [`8e1c755`](https://github.com/eea/volto-clms-theme/commit/8e1c7555b35fbeb49c3d78a8b47ad65b51b98e33)]
14
+
15
+ #### :hammer_and_wrench: Others
16
+
17
+ - fix icon and jest [ana-oprea - [`2566588`](https://github.com/eea/volto-clms-theme/commit/256658849faedcbfb406edd2d4bad2d1c4a287b7)]
18
+ - fix jest [ana-oprea - [`1c1d529`](https://github.com/eea/volto-clms-theme/commit/1c1d529940cb43bc02afe77686578cc181473a55)]
7
19
  ### [1.1.289](https://github.com/eea/volto-clms-theme/compare/1.1.288...1.1.289) - 21 May 2026
8
20
 
9
21
  #### :hammer_and_wrench: Others
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-clms-theme",
3
- "version": "1.1.289",
3
+ "version": "1.1.291",
4
4
  "description": "volto-clms-theme: Volto theme for CLMS site",
5
5
  "main": "src/index.js",
6
6
  "author": "CodeSyntax for the European Environment Agency",
@@ -12,6 +12,10 @@ const messages = defineMessages({
12
12
  id: 'Clear filters',
13
13
  defaultMessage: 'Clear filters',
14
14
  },
15
+ applied: {
16
+ id: 'Applied',
17
+ defaultMessage: 'Applied',
18
+ },
15
19
  });
16
20
 
17
21
  const FilterList = (props) => {
@@ -93,7 +97,34 @@ const FilterList = (props) => {
93
97
  .length;
94
98
 
95
99
  const intl = useIntl();
96
- return showFilterList && Object.keys(currentFilters).length ? (
100
+ if (!showFilterList || !Object.keys(currentFilters).length) return null;
101
+
102
+ if (props.variant === 'globalSearch') {
103
+ if (!totalFilters) return null;
104
+
105
+ return (
106
+ <div className="global-search-applied-filters">
107
+ <span>
108
+ {intl.formatMessage(messages.applied)}: {totalFilters}
109
+ </span>
110
+ <Button
111
+ icon
112
+ basic
113
+ compact
114
+ size="small"
115
+ aria-label={intl.formatMessage(messages.clearFilters)}
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ !isEditMode && setFacets({});
119
+ }}
120
+ >
121
+ <Icon name="trash" />
122
+ </Button>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ return (
97
128
  <div className="accordion ui filter-listing">
98
129
  <div
99
130
  className="filter-list-header"
@@ -118,7 +149,7 @@ const FilterList = (props) => {
118
149
  </Button>
119
150
  </div>
120
151
  </div>
121
- ) : null;
152
+ );
122
153
  };
123
154
 
124
155
  export default FilterList;
@@ -10,8 +10,10 @@ import FilterList from './FilterList';
10
10
  import CclFiltersModal from '@eeacms/volto-clms-theme/components/CclFiltersModal/CclFiltersModal';
11
11
  import { Icon } from '@plone/volto/components';
12
12
  import React from 'react';
13
+ import categoryFilterSVG from './icons/filter-symbol.svg';
13
14
  import filterSVG from '@plone/volto/icons/filter.svg';
14
15
  import { flushSync } from 'react-dom';
16
+ import { usesGlobalSearchDesign } from './searchDesign';
15
17
 
16
18
  const messages = defineMessages({
17
19
  searchButtonText: {
@@ -26,6 +28,78 @@ const FacetWrapper = ({ children }) => (
26
28
  </Segment>
27
29
  );
28
30
 
31
+ const getSortLabel = (value, querystring) => {
32
+ const title = querystring?.sortable_indexes?.[value]?.title || value;
33
+ if (
34
+ ['effective', 'publication_date', 'modified', 'resourceEffective'].includes(
35
+ value,
36
+ ) ||
37
+ /publication date/i.test(title)
38
+ ) {
39
+ return 'Time';
40
+ }
41
+ if (value === 'sortable_title') {
42
+ return 'Title';
43
+ }
44
+ if (value === 'relevance') {
45
+ return 'Relevance';
46
+ }
47
+ return title?.replace(/^Sort by /, '') || value;
48
+ };
49
+
50
+ const GlobalSortOn = ({
51
+ data,
52
+ querystring,
53
+ isEditMode,
54
+ sortOn,
55
+ sortOrder,
56
+ onChange,
57
+ }) => {
58
+ const configuredOptions = data.sortOnOptions || [];
59
+ const concreteOptions = configuredOptions.filter(
60
+ (option) => !['SearchableText', 'relevance'].includes(option),
61
+ );
62
+ const options = ['relevance', ...concreteOptions];
63
+ const activeSortOn = sortOn && options.includes(sortOn) ? sortOn : options[0];
64
+
65
+ if (!options.length) return null;
66
+
67
+ return (
68
+ <div className="sorting global-search-sorting">
69
+ <span className="global-search-sort-label">Sort by:</span>
70
+ {options.map((option) => {
71
+ const isActive = activeSortOn === option;
72
+ return (
73
+ <button
74
+ key={option}
75
+ type="button"
76
+ className={isActive ? 'active' : ''}
77
+ onClick={() => {
78
+ if (isEditMode) return;
79
+ if (option === 'relevance') {
80
+ onChange(null, null);
81
+ return;
82
+ }
83
+ const nextOrder =
84
+ isActive && sortOrder === 'ascending'
85
+ ? 'descending'
86
+ : 'ascending';
87
+ onChange(option, nextOrder);
88
+ }}
89
+ >
90
+ {getSortLabel(option, querystring)}
91
+ {isActive && option !== 'relevance' && (
92
+ <span className="global-search-sort-direction">
93
+ {sortOrder === 'descending' ? '▼' : '▲'}
94
+ </span>
95
+ )}
96
+ </button>
97
+ );
98
+ })}
99
+ </div>
100
+ );
101
+ };
102
+
29
103
  function setFacetsHandler(setFacets, onTriggerSearch, searchedText) {
30
104
  return (f) => {
31
105
  flushSync(() => {
@@ -57,6 +131,7 @@ const RightModalFacets = (props) => {
57
131
  } = props;
58
132
  const { showSearchButton } = data;
59
133
  const isLive = !showSearchButton;
134
+ const hasGlobalSearchDesign = usesGlobalSearchDesign(props);
60
135
  const intl = useIntl();
61
136
  // Should we generalize this to an external function?
62
137
  if (querystring?.sortable_indexes?.effective?.title) {
@@ -75,8 +150,63 @@ const RightModalFacets = (props) => {
75
150
  querystring.sortable_indexes.version.title = 'Version';
76
151
  }
77
152
 
153
+ const updateFacets = setFacetsHandler(
154
+ setFacets,
155
+ onTriggerSearch,
156
+ searchedText,
157
+ );
158
+
159
+ const facetsModal = data.facets?.length ? (
160
+ <CclFiltersModal
161
+ trigger={
162
+ <div className="filters-element">
163
+ <div className="filters-title">
164
+ {hasGlobalSearchDesign ? (
165
+ <span
166
+ className="global-search-categories-icon"
167
+ aria-hidden="true"
168
+ >
169
+ <Icon
170
+ className="global-search-categories-filter-icon"
171
+ name={categoryFilterSVG}
172
+ size="7px"
173
+ />
174
+ </span>
175
+ ) : (
176
+ <Icon className="ui" name={filterSVG} size={'20'} />
177
+ )}
178
+ <span className="filters-title-bold">
179
+ {hasGlobalSearchDesign ? 'Show Filters' : data.facetsTitle}
180
+ </span>
181
+ </div>
182
+ </div>
183
+ }
184
+ data={data}
185
+ setFacets={updateFacets}
186
+ >
187
+ <div id="right-modal-facets" className="facets">
188
+ <Facets
189
+ querystring={querystring}
190
+ data={data}
191
+ facets={facets}
192
+ isEditMode={isEditMode}
193
+ setFacets={updateFacets}
194
+ facetWrapper={FacetWrapper}
195
+ />
196
+ </div>
197
+ </CclFiltersModal>
198
+ ) : null;
199
+
78
200
  return (
79
- <Grid className="searchBlock-facets right-column-facets" stackable>
201
+ <Grid
202
+ className={[
203
+ 'searchBlock-facets right-column-facets',
204
+ hasGlobalSearchDesign ? 'global-search-facets' : '',
205
+ ]
206
+ .filter(Boolean)
207
+ .join(' ')}
208
+ stackable
209
+ >
80
210
  {data?.headline && (
81
211
  <Grid.Row>
82
212
  <Grid.Column>
@@ -91,21 +221,29 @@ const RightModalFacets = (props) => {
91
221
  ? data.showSearchInput
92
222
  : true) && (
93
223
  <>
94
- <div className="search-wrapper">
95
- <SearchInput {...props} isLive={isLive} />
96
- {data.showSearchButton && (
97
- <Button
98
- primary
99
- onClick={() => onTriggerSearch(searchText)}
100
- aria-label={
101
- data.searchButtonLabel ||
102
- intl.formatMessage(messages.searchButtonText)
103
- }
104
- >
105
- <span className="ccl-icon-zoom"></span>
106
- </Button>
107
- )}
108
- </div>
224
+ {hasGlobalSearchDesign ? (
225
+ <SearchInput
226
+ {...props}
227
+ isLive={isLive}
228
+ useSearchlibSearchDesign={hasGlobalSearchDesign}
229
+ />
230
+ ) : (
231
+ <div className="search-wrapper">
232
+ <SearchInput {...props} isLive={isLive} />
233
+ {data.showSearchButton && (
234
+ <Button
235
+ primary
236
+ onClick={() => onTriggerSearch(searchText)}
237
+ aria-label={
238
+ data.searchButtonLabel ||
239
+ intl.formatMessage(messages.searchButtonText)
240
+ }
241
+ >
242
+ <span className="ccl-icon-zoom"></span>
243
+ </Button>
244
+ )}
245
+ </div>
246
+ )}
109
247
  <div className="search-box-hint">
110
248
  <p>
111
249
  Hint: you can use double quotes to search for exact phrases.
@@ -115,84 +253,92 @@ const RightModalFacets = (props) => {
115
253
  </>
116
254
  )}
117
255
 
118
- <div>
119
- <FilterList
120
- {...props}
121
- isEditMode={isEditMode}
122
- setFacets={setFacetsHandler(
123
- setFacets,
124
- onTriggerSearch,
125
- searchedText,
126
- )}
127
- />
128
- </div>
129
-
130
- <div className="search-results-count-sort search-filters">
131
- <SearchDetails total={totalItems} data={data} />
132
- <div className="filters-container">
133
- {data.showSortOn && (
134
- <SortOn
135
- data={data}
136
- querystring={querystring}
256
+ {hasGlobalSearchDesign ? (
257
+ <>
258
+ <div className="global-search-count">
259
+ <SearchDetails total={totalItems} data={data} as="div" />
260
+ </div>
261
+ <div className="above-results global-search-above-results">
262
+ <div className="global-search-filters-left">
263
+ {facetsModal}
264
+ <FilterList
265
+ {...props}
266
+ variant="globalSearch"
267
+ isEditMode={isEditMode}
268
+ setFacets={updateFacets}
269
+ />
270
+ </div>
271
+ {data.showSortOn && (
272
+ <GlobalSortOn
273
+ data={data}
274
+ querystring={querystring}
275
+ isEditMode={isEditMode}
276
+ sortOrder={sortOrder}
277
+ sortOn={sortOn}
278
+ onChange={(sortOnParam, sortOrderParam) => {
279
+ flushSync(() => {
280
+ setSortOn(sortOnParam || undefined);
281
+ setSortOrder(sortOrderParam || undefined);
282
+ onTriggerSearch(
283
+ searchedText || '',
284
+ facets,
285
+ sortOnParam,
286
+ sortOrderParam,
287
+ );
288
+ });
289
+ }}
290
+ />
291
+ )}
292
+ </div>
293
+ </>
294
+ ) : (
295
+ <>
296
+ <div>
297
+ <FilterList
298
+ {...props}
137
299
  isEditMode={isEditMode}
138
- sortOrder={sortOrder}
139
- sortOn={sortOn}
140
- setSortOn={(sortOnParam) => {
141
- flushSync(() => {
142
- setSortOn(sortOnParam);
143
- onTriggerSearch(searchedText || '', facets, sortOnParam);
144
- });
145
- }}
146
- setSortOrder={(sortOrderParam) => {
147
- flushSync(() => {
148
- setSortOrder(sortOrderParam);
149
- onTriggerSearch(
150
- searchedText || '',
151
- facets,
152
- sortOn,
153
- sortOrderParam,
154
- );
155
- });
156
- }}
300
+ setFacets={updateFacets}
157
301
  />
158
- )}
159
- {data.facets?.length && (
160
- <CclFiltersModal
161
- trigger={
162
- <div className="filters-element">
163
- <div className="filters-title">
164
- <Icon className="ui" name={filterSVG} size={'20'} />
165
- <span className="filters-title-bold">
166
- {data.facetsTitle}
167
- </span>
168
- </div>
169
- </div>
170
- }
171
- data={data}
172
- setFacets={setFacetsHandler(
173
- setFacets,
174
- onTriggerSearch,
175
- searchedText,
176
- )}
177
- >
178
- <div id="right-modal-facets" className="facets">
179
- <Facets
180
- querystring={querystring}
302
+ </div>
303
+
304
+ <div className="search-results-count-sort search-filters">
305
+ <SearchDetails total={totalItems} data={data} />
306
+ <div className="filters-container">
307
+ {data.showSortOn && (
308
+ <SortOn
181
309
  data={data}
182
- facets={facets}
310
+ querystring={querystring}
183
311
  isEditMode={isEditMode}
184
- setFacets={setFacetsHandler(
185
- setFacets,
186
- onTriggerSearch,
187
- searchedText,
188
- )}
189
- facetWrapper={FacetWrapper}
312
+ sortOrder={sortOrder}
313
+ sortOn={sortOn}
314
+ setSortOn={(sortOnParam) => {
315
+ flushSync(() => {
316
+ setSortOn(sortOnParam);
317
+ onTriggerSearch(
318
+ searchedText || '',
319
+ facets,
320
+ sortOnParam,
321
+ );
322
+ });
323
+ }}
324
+ setSortOrder={(sortOrderParam) => {
325
+ flushSync(() => {
326
+ setSortOrder(sortOrderParam);
327
+ onTriggerSearch(
328
+ searchedText || '',
329
+ facets,
330
+ sortOn,
331
+ sortOrderParam,
332
+ );
333
+ });
334
+ }}
190
335
  />
191
- </div>
192
- </CclFiltersModal>
193
- )}
194
- </div>
195
- </div>
336
+ )}
337
+ {facetsModal}
338
+ </div>
339
+ </div>
340
+ </>
341
+ )}
196
342
  {children}
197
343
  </Grid.Column>
198
344
  </Grid.Row>
@@ -1,8 +1,9 @@
1
1
  import React from 'react';
2
- import { Button, Input } from 'semantic-ui-react';
2
+ import { Button, Icon as SemanticIcon, Input } from 'semantic-ui-react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
- import { Icon } from '@plone/volto/components';
4
+ import { Icon as VoltoIcon } from '@plone/volto/components';
5
5
  import clearSVG from '@plone/volto/icons/clear.svg';
6
+ import searchSVG from './icons/search.svg';
6
7
 
7
8
  const messages = defineMessages({
8
9
  search: {
@@ -12,8 +13,86 @@ const messages = defineMessages({
12
13
  });
13
14
 
14
15
  const SearchInput = (props) => {
15
- const { data, searchText, setSearchText, isLive, onTriggerSearch } = props;
16
+ const {
17
+ data,
18
+ searchText,
19
+ setSearchText,
20
+ isLive,
21
+ isGlobalSearch,
22
+ useSearchlibSearchDesign = isGlobalSearch,
23
+ onTriggerSearch,
24
+ } = props;
16
25
  const intl = useIntl();
26
+ const placeholder =
27
+ data.searchInputPrompt || intl.formatMessage(messages.search);
28
+
29
+ if (useSearchlibSearchDesign) {
30
+ return (
31
+ <div className="sui-search-box">
32
+ <div className="search-input">
33
+ <div className="terms-box">
34
+ <input
35
+ maxLength="200"
36
+ id={`${props.id}-searchtext`}
37
+ value={searchText}
38
+ className=""
39
+ placeholder={placeholder}
40
+ enterKeyHint="search"
41
+ onKeyDown={(event) => {
42
+ if (event.key === 'Enter') {
43
+ onTriggerSearch(searchText);
44
+ }
45
+ }}
46
+ onChange={(event) => {
47
+ setSearchText(event.target.value);
48
+ if (isLive) {
49
+ onTriggerSearch(event.target.value);
50
+ }
51
+ }}
52
+ />
53
+
54
+ <div className="terms-box-left">
55
+ <div className="input-controls">
56
+ {searchText && (
57
+ <div className="ui button basic clear-button">
58
+ <SemanticIcon
59
+ tabIndex={0}
60
+ name="close"
61
+ role="button"
62
+ onClick={() => {
63
+ setSearchText('');
64
+ onTriggerSearch('');
65
+ }}
66
+ onKeyDown={(event) => {
67
+ if (event.key === 'Enter') {
68
+ setSearchText('');
69
+ onTriggerSearch('');
70
+ }
71
+ }}
72
+ />
73
+ </div>
74
+ )}
75
+ </div>
76
+
77
+ <div
78
+ tabIndex={0}
79
+ role="button"
80
+ className="search-icon"
81
+ onClick={() => onTriggerSearch(searchText)}
82
+ onKeyDown={(event) => {
83
+ if (event.key === 'Enter') {
84
+ onTriggerSearch(searchText);
85
+ }
86
+ }}
87
+ >
88
+ <VoltoIcon name={searchSVG} size="29px" />
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
17
96
 
18
97
  return (
19
98
  <div className="search-input">
@@ -21,9 +100,7 @@ const SearchInput = (props) => {
21
100
  maxLength="200"
22
101
  id={`${props.id}-searchtext`}
23
102
  value={searchText}
24
- placeholder={
25
- data.searchInputPrompt || intl.formatMessage(messages.search)
26
- }
103
+ placeholder={placeholder}
27
104
  fluid
28
105
  onKeyPress={(event) => {
29
106
  if (isLive || event.key === 'Enter') onTriggerSearch(searchText);
@@ -46,7 +123,7 @@ const SearchInput = (props) => {
46
123
  onTriggerSearch('');
47
124
  }}
48
125
  >
49
- <Icon name={clearSVG}></Icon>
126
+ <VoltoIcon name={clearSVG}></VoltoIcon>
50
127
  </Button>
51
128
  )}
52
129
  </div>
@@ -0,0 +1,5 @@
1
+ <svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H9V2H1V1Z" fill="white" style="fill:white;fill-opacity:1;" />
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M3 5H7V6H3V5Z" fill="white" style="fill:white;fill-opacity:1;" />
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M2 3V4H8V3H2Z" fill="white" style="fill:white;fill-opacity:1;" />
5
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M37.3757 34.3789L30.2003 27.2246C32.5154 24.2751 33.7716 20.633 33.7669 16.8835C33.7669 13.5442 32.7767 10.28 30.9215 7.50351C29.0663 4.72705 26.4295 2.56305 23.3445 1.28518C20.2594 0.00731515 16.8647 -0.327033 13.5897 0.324418C10.3146 0.97587 7.30625 2.58386 4.94506 4.94506C2.58386 7.30625 0.97587 10.3146 0.324418 13.5897C-0.327033 16.8647 0.00731528 20.2594 1.28518 23.3445C2.56305 26.4295 4.72705 29.0664 7.50351 30.9215C10.28 32.7767 13.5442 33.7669 16.8835 33.7669C20.633 33.7716 24.2751 32.5154 27.2246 30.2003L34.3789 37.3757C34.5751 37.5735 34.8085 37.7306 35.0657 37.8377C35.3229 37.9448 35.5987 38 35.8773 38C36.1559 38 36.4318 37.9448 36.689 37.8377C36.9461 37.7306 37.1795 37.5735 37.3757 37.3757C37.5735 37.1795 37.7305 36.9461 37.8377 36.689C37.9448 36.4318 38 36.1559 38 35.8773C38 35.5987 37.9448 35.3229 37.8377 35.0657C37.7305 34.8085 37.5735 34.5751 37.3757 34.3789ZM4.22087 16.8835C4.22087 14.379 4.96352 11.9309 6.3549 9.8485C7.74628 7.76615 9.72391 6.14315 12.0377 5.18475C14.3515 4.22635 16.8975 3.97559 19.3538 4.46418C21.8101 4.95277 24.0664 6.15876 25.8373 7.92966C27.6081 9.70055 28.8141 11.9568 29.3027 14.4131C29.7913 16.8694 29.5406 19.4154 28.5822 21.7292C27.6238 24.043 26.0008 26.0206 23.9184 27.412C21.8361 28.8034 19.3879 29.546 16.8835 29.546C13.5251 29.546 10.3043 28.2119 7.92966 25.8373C5.55496 23.4626 4.22087 20.2418 4.22087 16.8835Z" />
3
+ </svg>
@@ -0,0 +1,8 @@
1
+ export const usesGlobalSearchDesign = ({ data, location, path }) => {
2
+ const currentPath = location?.pathname || path || '';
3
+ const normalizedPath = currentPath.replace(/\/$/, '');
4
+ return (
5
+ data?.listingBodyTemplate === 'CclGlobalSearch' ||
6
+ normalizedPath.endsWith('/dataset-catalog')
7
+ );
8
+ };
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
2
2
  import { FormattedMessage } from 'react-intl';
3
3
  import { useSelector } from 'react-redux';
4
4
  import { Link } from 'react-router-dom';
5
- import { Icon, Label } from 'semantic-ui-react';
5
+ import { Icon } from 'semantic-ui-react';
6
6
 
7
7
  import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
8
8
  import { UniversalLink, Icon as VoltoIcon } from '@plone/volto/components';
@@ -59,7 +59,7 @@ const CardLink = ({ url, children, className, condition = true }) => {
59
59
  );
60
60
  };
61
61
 
62
- const DocCard = ({ card, url, showEditor, children }) => {
62
+ const DocCard = ({ card, url, showEditor, hideSize = false, children }) => {
63
63
  return (
64
64
  <>
65
65
  <div className="card-doc-header">
@@ -104,7 +104,7 @@ const DocCard = ({ card, url, showEditor, children }) => {
104
104
  </Link>
105
105
  )}
106
106
  </div>
107
- {card?.['@type'] === 'TechnicalLibrary' && (
107
+ {card?.['@type'] === 'TechnicalLibrary' && !hideSize && (
108
108
  <div className="card-doc-size">{card.getObjSize || ''}</div>
109
109
  )}
110
110
  </div>
@@ -317,16 +317,30 @@ function CclCard(props) {
317
317
  </>
318
318
  )}
319
319
  {type === 'globalSearch' && (
320
- <>
321
- <Label ribbon="right" color="olive">
322
- {content_type}
323
- </Label>
324
- <DocCard card={card} url={url} showEditor={showEditor}>
325
- {children}
320
+ <div className="global-search-result-row">
321
+ <div className="global-search-result-main">
322
+ <DocCard
323
+ card={card}
324
+ url={url}
325
+ showEditor={showEditor}
326
+ hideSize
327
+ >
328
+ {children}
326
329
 
327
- <SearchResultExtras content={card} />
328
- </DocCard>
329
- </>
330
+ <SearchResultExtras content={card} />
331
+ </DocCard>
332
+ </div>
333
+ <div className="global-search-result-side">
334
+ <span className="global-search-result-type">
335
+ {content_type}
336
+ </span>
337
+ {card?.['@type'] === 'TechnicalLibrary' && (
338
+ <span className="global-search-result-size">
339
+ {card.getObjSize || ''}
340
+ </span>
341
+ )}
342
+ </div>
343
+ </div>
330
344
  )}
331
345
  {(type === 'block' || type === 'threeColumns') && (
332
346
  <>
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
2
 
3
3
  import ListingBody from '@plone/volto/components/manage/Blocks/Listing/ListingBody';
4
- import { withBlockExtensions } from '@plone/volto/helpers';
4
+ import { BodyClass, withBlockExtensions } from '@plone/volto/helpers';
5
5
 
6
6
  import config from '@plone/volto/registry';
7
+ import { usesGlobalSearchDesign } from '@eeacms/volto-clms-theme/components/Blocks/CustomTemplates/VoltoSearchBlock/searchDesign';
7
8
 
8
9
  import {
9
10
  withSearch,
@@ -63,7 +64,12 @@ const applyDefaults = (data, root) => {
63
64
  };
64
65
 
65
66
  const SearchBlockView = (props) => {
66
- const { data, searchData, mode = 'view', variation } = props;
67
+ const { data, searchData, mode = 'view', variation, location, path } = props;
68
+ const hasGlobalSearchDesign = usesGlobalSearchDesign({
69
+ data,
70
+ location,
71
+ path,
72
+ });
67
73
 
68
74
  const Layout = variation.view;
69
75
 
@@ -86,8 +92,17 @@ const SearchBlockView = (props) => {
86
92
  const { variations } = config.blocks.blocksConfig.listing;
87
93
  const listingBodyVariation = variations.find(({ id }) => id === selectedView);
88
94
 
89
- return (
90
- <div className="block search">
95
+ const searchBlock = (
96
+ <div
97
+ className={[
98
+ 'block search',
99
+ hasGlobalSearchDesign
100
+ ? 'global-search-block searchlib-block searchapp searchapp-clmsSearchTechnicalLibrary'
101
+ : '',
102
+ ]
103
+ .filter(Boolean)
104
+ .join(' ')}
105
+ >
91
106
  <Layout
92
107
  {...props}
93
108
  isEditMode={mode === 'edit'}
@@ -103,6 +118,14 @@ const SearchBlockView = (props) => {
103
118
  </Layout>
104
119
  </div>
105
120
  );
121
+
122
+ return hasGlobalSearchDesign ? (
123
+ <BodyClass className="global-search-page searchlib-page">
124
+ {searchBlock}
125
+ </BodyClass>
126
+ ) : (
127
+ searchBlock
128
+ );
106
129
  };
107
130
 
108
131
  export const SearchBlockViewComponent = compose(
@@ -35,6 +35,7 @@ function getInitialState(
35
35
  id,
36
36
  sortOnParam,
37
37
  sortOrderParam,
38
+ useDefaultSort = true,
38
39
  ) {
39
40
  const {
40
41
  types: facetWidgetTypes,
@@ -69,8 +70,18 @@ function getInitialState(
69
70
  ]
70
71
  : []),
71
72
  ],
72
- sort_on: sortOnParam || data.query?.sort_on,
73
- sort_order: sortOrderParam || data.query?.sort_order,
73
+ sort_on:
74
+ sortOnParam !== undefined
75
+ ? sortOnParam
76
+ : useDefaultSort
77
+ ? data.query?.sort_on
78
+ : undefined,
79
+ sort_order:
80
+ sortOrderParam !== undefined
81
+ ? sortOrderParam
82
+ : useDefaultSort
83
+ ? data.query?.sort_order
84
+ : undefined,
74
85
  b_size: data.query?.b_size,
75
86
  limit: data.query?.limit,
76
87
  block: id,
@@ -92,6 +103,7 @@ function normalizeState({
92
103
  sortOn,
93
104
  sortOrder,
94
105
  facetSettings, // data.facets extracted from block data
106
+ useDefaultSort = true,
95
107
  }) {
96
108
  const {
97
109
  types: facetWidgetTypes,
@@ -115,8 +127,18 @@ function normalizeState({
115
127
  return valueToQuery({ value, facet });
116
128
  }),
117
129
  ].filter((o) => !!o),
118
- sort_on: sortOn || query.sort_on,
119
- sort_order: sortOrder || query.sort_order,
130
+ sort_on:
131
+ sortOn !== undefined
132
+ ? sortOn
133
+ : useDefaultSort
134
+ ? query.sort_on
135
+ : undefined,
136
+ sort_order:
137
+ sortOrder !== undefined
138
+ ? sortOrder
139
+ : useDefaultSort
140
+ ? query.sort_order
141
+ : undefined,
120
142
  b_size: query.b_size,
121
143
  limit: query.limit,
122
144
  block: id,
@@ -196,6 +218,13 @@ const useHashState = () => {
196
218
  }
197
219
  });
198
220
 
221
+ SEARCH_ENDPOINT_FIELDS.forEach((k) => {
222
+ if (!searchData[k] && oldState[k]) {
223
+ delete newParams[k];
224
+ changed = true;
225
+ }
226
+ });
227
+
199
228
  if (changed) {
200
229
  history.push({
201
230
  search: qs.stringify(newParams),
@@ -240,6 +269,7 @@ const withSearch = (options) => (WrappedComponent) => {
240
269
 
241
270
  function WithSearch(props) {
242
271
  const { data, id, editable = false } = props;
272
+ const isGlobalSearch = data?.listingBodyTemplate === 'CclGlobalSearch';
243
273
 
244
274
  const [locationSearchData, setLocationSearchData] = useSearchBlockState(
245
275
  id,
@@ -323,11 +353,25 @@ const withSearch = (options) => (WrappedComponent) => {
323
353
  previousUrlQuery,
324
354
  ]);
325
355
 
326
- const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
327
- const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
356
+ const [sortOn, setSortOn] = React.useState(
357
+ locationSearchData.sort_on ||
358
+ (isGlobalSearch ? undefined : data?.query?.sort_on),
359
+ );
360
+ const [sortOrder, setSortOrder] = React.useState(
361
+ locationSearchData.sort_order ||
362
+ (isGlobalSearch ? undefined : data?.query?.sort_order),
363
+ );
328
364
 
329
365
  const [searchData, setSearchData] = React.useState(
330
- getInitialState(data, facets, urlSearchText, id),
366
+ getInitialState(
367
+ data,
368
+ facets,
369
+ urlSearchText,
370
+ id,
371
+ sortOn,
372
+ sortOrder,
373
+ !isGlobalSearch,
374
+ ),
331
375
  );
332
376
 
333
377
  const deepFacets = JSON.stringify(facets);
@@ -341,9 +385,18 @@ const withSearch = (options) => (WrappedComponent) => {
341
385
  id,
342
386
  sortOn,
343
387
  sortOrder,
388
+ !isGlobalSearch,
344
389
  ),
345
390
  );
346
- }, [deepData, deepFacets, urlSearchText, id, sortOn, sortOrder]);
391
+ }, [
392
+ deepData,
393
+ deepFacets,
394
+ urlSearchText,
395
+ id,
396
+ sortOn,
397
+ sortOrder,
398
+ isGlobalSearch,
399
+ ]);
347
400
 
348
401
  const timeoutRef = React.useRef();
349
402
  const facetSettings = data?.facets;
@@ -359,18 +412,32 @@ const withSearch = (options) => (WrappedComponent) => {
359
412
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
360
413
  timeoutRef.current = setTimeout(
361
414
  () => {
415
+ const shouldClearSort = toSortOn === null;
416
+ const hasSortOnParam = toSortOn !== undefined;
417
+ const nextSortOn = shouldClearSort
418
+ ? undefined
419
+ : hasSortOnParam
420
+ ? toSortOn
421
+ : sortOn;
422
+ const nextSortOrder = shouldClearSort
423
+ ? undefined
424
+ : toSortOrder !== undefined
425
+ ? toSortOrder
426
+ : sortOrder;
362
427
  const newSearchData = normalizeState({
363
428
  id,
364
429
  query: data.query || {},
365
430
  facets: toSearchFacets || facets,
366
431
  searchText: toSearchText ? toSearchText.trim() : '',
367
- sortOn: toSortOn || sortOn,
368
- sortOrder: toSortOrder || sortOrder,
432
+ sortOn: nextSortOn,
433
+ sortOrder: nextSortOrder,
369
434
  facetSettings,
435
+ useDefaultSort: !isGlobalSearch,
370
436
  });
371
437
  if (toSearchFacets) setFacets(toSearchFacets);
372
- if (toSortOn) setSortOn(toSortOn);
373
- if (toSortOrder) setSortOrder(toSortOrder);
438
+ if (hasSortOnParam) setSortOn(toSortOn || undefined);
439
+ if (toSortOrder !== undefined)
440
+ setSortOrder(toSortOrder || undefined);
374
441
  setSearchData(newSearchData);
375
442
  setLocationSearchData(getSearchFields(newSearchData));
376
443
  },
@@ -388,6 +455,7 @@ const withSearch = (options) => (WrappedComponent) => {
388
455
  sortOn,
389
456
  sortOrder,
390
457
  facetSettings,
458
+ isGlobalSearch,
391
459
  ],
392
460
  );
393
461
 
@@ -289,6 +289,456 @@
289
289
  font-size: 0.8rem;
290
290
  }
291
291
 
292
+ body.global-search-page h1.documentFirstHeading {
293
+ padding-bottom: 1rem;
294
+ border-bottom: 1px solid @clmsGreen;
295
+ margin-bottom: 0.7rem;
296
+ color: #273b4b;
297
+ font-size: 32px;
298
+ line-height: 1.15;
299
+ }
300
+
301
+ body.global-search-page .block.search.global-search-block {
302
+ margin-top: 0;
303
+ }
304
+
305
+ body.global-search-page.section-global-search #page-document > p {
306
+ margin-bottom: 0;
307
+ line-height: 1.35;
308
+ }
309
+
310
+ body.global-search-page.section-global-search #page-document > p strong:empty {
311
+ display: none;
312
+ }
313
+
314
+ body.global-search-page.section-global-search
315
+ #page-document
316
+ > p:has(strong:empty) {
317
+ display: none;
318
+ }
319
+
320
+ body.global-search-page.section-global-search
321
+ .block.search.global-search-block {
322
+ margin-top: 3rem;
323
+ }
324
+
325
+ body.global-search-page.section-dataset-catalog
326
+ .block.search.global-search-block {
327
+ margin-top: 1.5rem;
328
+ }
329
+
330
+ body.global-search-page .block.search.global-search-block .ui.grid {
331
+ margin: 0;
332
+ }
333
+
334
+ body.global-search-page .block.search.global-search-block .ui.grid > .row {
335
+ padding-top: 0;
336
+ }
337
+
338
+ body.global-search-page
339
+ .block.search.global-search-block
340
+ .ui.grid
341
+ > .row
342
+ > .column {
343
+ padding-right: 0;
344
+ padding-left: 0;
345
+ }
346
+
347
+ body.global-search-page .block.search.global-search-block .sui-search-box {
348
+ margin: 0 0 0.6rem;
349
+ }
350
+
351
+ body.global-search-page .block.search.global-search-block .search-box-hint p {
352
+ margin: 0;
353
+ color: #273b4b;
354
+ font-size: 0.75rem;
355
+ }
356
+
357
+ body.global-search-page .block.search.global-search-block .global-search-count {
358
+ padding-bottom: 0.5rem;
359
+ border-bottom: 2px solid fade(#273b4b, 20%);
360
+ margin-top: 1.75rem;
361
+ }
362
+
363
+ body.global-search-page
364
+ .block.search.global-search-block
365
+ .global-search-count
366
+ .search-details {
367
+ margin: 0;
368
+ color: #273b4b;
369
+ font-size: 0.95rem;
370
+ font-weight: 700;
371
+ }
372
+
373
+ body.global-search-page
374
+ .block.search.global-search-block
375
+ .global-search-above-results {
376
+ display: flex;
377
+ align-items: center;
378
+ justify-content: space-between;
379
+ padding: 0.55rem 0 1.4rem;
380
+ margin: 0;
381
+ }
382
+
383
+ body.global-search-page
384
+ .block.search.global-search-block
385
+ .global-search-filters-left,
386
+ body.global-search-page
387
+ .block.search.global-search-block
388
+ .global-search-applied-filters,
389
+ body.global-search-page
390
+ .block.search.global-search-block
391
+ .global-search-sorting {
392
+ display: flex;
393
+ align-items: center;
394
+ }
395
+
396
+ body.global-search-page
397
+ .block.search.global-search-block
398
+ .global-search-filters-left {
399
+ gap: 1rem;
400
+ }
401
+
402
+ body.global-search-page
403
+ .block.search.global-search-block
404
+ .global-search-filters-left
405
+ .filters-element {
406
+ margin-left: 0;
407
+ }
408
+
409
+ body.global-search-page
410
+ .block.search.global-search-block
411
+ .global-search-filters-left
412
+ .filters-title {
413
+ align-items: center;
414
+ }
415
+
416
+ body.global-search-page
417
+ .block.search.global-search-block
418
+ .global-search-filters-left
419
+ .filters-title
420
+ .global-search-categories-icon {
421
+ display: inline-flex;
422
+ width: 16px !important;
423
+ height: 16px !important;
424
+ align-items: center;
425
+ justify-content: center;
426
+ margin-right: 0.35rem;
427
+ background: @clmsGreen;
428
+ border-radius: 50%;
429
+ color: #fff;
430
+ }
431
+
432
+ body.global-search-page
433
+ .block.search.global-search-block
434
+ .global-search-filters-left
435
+ .global-search-categories-icon
436
+ .global-search-categories-filter-icon {
437
+ display: block;
438
+ color: #fff;
439
+ }
440
+
441
+ body.global-search-page
442
+ .block.search.global-search-block
443
+ .global-search-applied-filters {
444
+ color: #808080;
445
+ gap: 0.25rem;
446
+ }
447
+
448
+ body.global-search-page
449
+ .block.search.global-search-block
450
+ .global-search-applied-filters
451
+ .ui.basic.button {
452
+ padding: 0 !important;
453
+ border: none;
454
+ box-shadow: none !important;
455
+ color: #808080 !important;
456
+ }
457
+
458
+ body.global-search-page
459
+ .block.search.global-search-block
460
+ .global-search-sorting {
461
+ gap: 0.7rem;
462
+ }
463
+
464
+ body.global-search-page
465
+ .block.search.global-search-block
466
+ .global-search-sort-label {
467
+ color: #273b4b;
468
+ }
469
+
470
+ body.global-search-page
471
+ .block.search.global-search-block
472
+ .global-search-sorting
473
+ button {
474
+ padding: 0;
475
+ border: 0;
476
+ background: transparent;
477
+ color: #808080;
478
+ cursor: pointer;
479
+ font: inherit;
480
+ font-weight: 700;
481
+ }
482
+
483
+ body.global-search-page
484
+ .block.search.global-search-block
485
+ .global-search-sorting
486
+ button.active,
487
+ body.global-search-page
488
+ .block.search.global-search-block
489
+ .global-search-sorting
490
+ button:hover {
491
+ color: @clmsGreen;
492
+ }
493
+
494
+ body.global-search-page
495
+ .block.search.global-search-block
496
+ .global-search-sort-direction {
497
+ margin-left: 0.25rem;
498
+ color: #808080;
499
+ font-size: 0.75rem;
500
+ }
501
+
502
+ body.global-search-page .block.search.global-search-block .card-doc {
503
+ padding: 0;
504
+ border-bottom: 1px solid #cfd5d8;
505
+ }
506
+
507
+ body.global-search-page
508
+ .block.search.global-search-block
509
+ .global-search-result-row {
510
+ display: grid;
511
+ padding: 1.15rem 0 1.05rem;
512
+ column-gap: 1.5rem;
513
+ grid-template-columns: minmax(0, 1fr) auto;
514
+ }
515
+
516
+ body.global-search-page
517
+ .block.search.global-search-block
518
+ .global-search-result-main {
519
+ min-width: 0;
520
+ }
521
+
522
+ body.global-search-page
523
+ .block.search.global-search-block
524
+ .global-search-result-main
525
+ .card-doc-header {
526
+ display: block;
527
+ }
528
+
529
+ body.global-search-page
530
+ .block.search.global-search-block
531
+ .global-search-result-main
532
+ .card-doc-title {
533
+ margin-bottom: 0.85rem;
534
+ font-size: 20px;
535
+ line-height: 1.25;
536
+ }
537
+
538
+ body.global-search-page
539
+ .block.search.global-search-block
540
+ .global-search-result-main
541
+ .card-doc-title
542
+ a {
543
+ color: @clmsGreen;
544
+ }
545
+
546
+ body.global-search-page
547
+ .block.search.global-search-block
548
+ .global-search-result-main
549
+ .card-doc-description {
550
+ margin-bottom: 0.35rem;
551
+ }
552
+
553
+ body.global-search-page
554
+ .block.search.global-search-block
555
+ .global-search-result-main
556
+ .card-doc-extrametadata {
557
+ color: #34495e;
558
+ font-size: 16px;
559
+ }
560
+
561
+ body.global-search-page
562
+ .block.search.global-search-block
563
+ .global-search-result-side {
564
+ display: flex;
565
+ flex-direction: column;
566
+ align-items: flex-end;
567
+ padding-top: 0.05rem;
568
+ color: #34495e;
569
+ gap: 1.15rem;
570
+ white-space: nowrap;
571
+ }
572
+
573
+ body.global-search-page
574
+ .block.search.global-search-block
575
+ .global-search-result-type {
576
+ padding: 0.25rem 0.55rem;
577
+ background: #e6e6e6;
578
+ border-radius: 4px;
579
+ color: #555;
580
+ font-size: 12px;
581
+ line-height: 1;
582
+ }
583
+
584
+ body.global-search-page
585
+ .block.search.global-search-block
586
+ .global-search-result-size {
587
+ font-size: 14px;
588
+ }
589
+
590
+ @media (max-width: 1127px) {
591
+ body.global-search-page.searchlib-page div#page-document.ui.container,
592
+ body.global-search-page.searchlib-page div#page-document {
593
+ width: calc(100% - 2rem) !important;
594
+ max-width: calc(100% - 2rem) !important;
595
+ padding-right: 1rem;
596
+ padding-left: 1rem;
597
+ }
598
+
599
+ body.global-search-page .block.search.global-search-block {
600
+ padding-right: 0;
601
+ padding-left: 0;
602
+ }
603
+ }
604
+
605
+ @media (max-width: 767px) {
606
+ body.global-search-page.searchlib-page div#page-document.ui.container,
607
+ body.global-search-page.searchlib-page div#page-document {
608
+ padding-right: 0.75rem;
609
+ padding-left: 0.75rem;
610
+ }
611
+
612
+ body.global-search-page h1.documentFirstHeading {
613
+ padding-bottom: 0.75rem;
614
+ margin-bottom: 0.75rem;
615
+ font-size: 28px;
616
+ }
617
+
618
+ body.global-search-page
619
+ .block.search.global-search-block
620
+ .sui-search-box
621
+ .search-input
622
+ .terms-box
623
+ input {
624
+ min-width: 0;
625
+ }
626
+
627
+ body.global-search-page
628
+ .block.search.global-search-block
629
+ .sui-search-box
630
+ .search-input
631
+ .terms-box
632
+ .input-controls
633
+ .clear-button {
634
+ width: 24px;
635
+ height: 24px;
636
+ margin: 0 0.35rem 0.35rem 0;
637
+ }
638
+
639
+ body.global-search-page .block.search.global-search-block .search-box-hint p {
640
+ font-size: 0.72rem;
641
+ line-height: 1.35;
642
+ }
643
+
644
+ body.global-search-page
645
+ .block.search.global-search-block
646
+ .global-search-count {
647
+ margin-top: 1.1rem;
648
+ }
649
+
650
+ body.global-search-page
651
+ .block.search.global-search-block
652
+ .global-search-above-results,
653
+ body.global-search-page
654
+ .block.search.global-search-block
655
+ .global-search-result-row {
656
+ display: flex;
657
+ flex-direction: column;
658
+ gap: 0.75rem;
659
+ }
660
+
661
+ body.global-search-page
662
+ .block.search.global-search-block
663
+ .global-search-above-results {
664
+ align-items: stretch;
665
+ padding-bottom: 1rem;
666
+ }
667
+
668
+ body.global-search-page
669
+ .block.search.global-search-block
670
+ .global-search-filters-left {
671
+ flex-wrap: wrap;
672
+ align-items: flex-start;
673
+ gap: 0.5rem 0.85rem;
674
+ }
675
+
676
+ body.global-search-page
677
+ .block.search.global-search-block
678
+ .global-search-sorting {
679
+ width: 100%;
680
+ flex-wrap: nowrap;
681
+ align-items: center;
682
+ justify-content: flex-start;
683
+ gap: 0.45rem 0.7rem;
684
+ }
685
+
686
+ body.global-search-page
687
+ .block.search.global-search-block
688
+ .global-search-result-row {
689
+ padding: 0.95rem 0;
690
+ }
691
+
692
+ body.global-search-page
693
+ .block.search.global-search-block
694
+ .global-search-result-main
695
+ .card-doc-title {
696
+ margin-bottom: 0.55rem;
697
+ font-size: 18px;
698
+ line-height: 1.3;
699
+ }
700
+
701
+ body.global-search-page
702
+ .block.search.global-search-block
703
+ .global-search-result-main
704
+ .card-doc-extrametadata {
705
+ font-size: 14px;
706
+ line-height: 1.4;
707
+ }
708
+
709
+ body.global-search-page
710
+ .block.search.global-search-block
711
+ .global-search-result-side {
712
+ flex-direction: row;
713
+ flex-wrap: wrap;
714
+ align-items: flex-start;
715
+ gap: 0.5rem;
716
+ white-space: normal;
717
+ }
718
+
719
+ body.global-search-page
720
+ .block.search.global-search-block
721
+ .ui.pagination.menu {
722
+ width: 100%;
723
+ max-width: 100%;
724
+ justify-content: flex-start;
725
+ overflow-x: auto;
726
+ }
727
+
728
+ body.global-search-page
729
+ .block.search.global-search-block
730
+ .ui.pagination.menu
731
+ .item {
732
+ flex: 0 0 auto;
733
+ }
734
+ }
735
+
736
+ @media (max-width: 420px) {
737
+ body.global-search-page h1.documentFirstHeading {
738
+ font-size: 26px;
739
+ }
740
+ }
741
+
292
742
  /* Filters */
293
743
  .filters-container {
294
744
  display: flex;