@eeacms/volto-eea-website-theme 1.28.3 → 1.30.0

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/.eslintrc.js CHANGED
@@ -48,12 +48,7 @@ const defaultConfig = {
48
48
  },
49
49
  },
50
50
  rules: {
51
- 'react/jsx-no-target-blank': [
52
- 'error',
53
- {
54
- allowReferrer: true,
55
- },
56
- ],
51
+ 'react/jsx-no-target-blank': 'off',
57
52
  },
58
53
  };
59
54
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ 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.30.0](https://github.com/eea/volto-eea-website-theme/compare/1.29.0...1.30.0) - 13 March 2024
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: set isPrint redux state when window.print() is triggered [laszlocseh - [`4d07b9a`](https://github.com/eea/volto-eea-website-theme/commit/4d07b9a76af0e43340e100db10ad1051c61f3a89)]
12
+
13
+ #### :bug: Bug Fixes
14
+
15
+ - fix: smaller timeout in window.print [laszlocseh - [`808d7e9`](https://github.com/eea/volto-eea-website-theme/commit/808d7e9ef406f38e008a718fd4883fe735c296de)]
16
+
17
+ #### :house: Internal changes
18
+
19
+ - chore: cleanup unused RemoveSchema code [laszlocseh - [`6d8de0a`](https://github.com/eea/volto-eea-website-theme/commit/6d8de0a27dd6bfd5645bb730d2c66f2d4784c158)]
20
+ - chore: cleanup unused RemoveSchema code [laszlocseh - [`c1f2650`](https://github.com/eea/volto-eea-website-theme/commit/c1f2650f0f7ffb27c168399fabcbf69d0299235b)]
21
+
22
+ #### :hammer_and_wrench: Others
23
+
24
+ - Update package.json [ichim-david - [`4c2f794`](https://github.com/eea/volto-eea-website-theme/commit/4c2f794256098dff98aa4b78ba99be65709b1b9b)]
25
+ - Fix export statement in reducers/index.js [David Ichim - [`bbaf5be`](https://github.com/eea/volto-eea-website-theme/commit/bbaf5be2e7cd24b6a8c5d694ee4030b6fafbd120)]
26
+ - Lint fix, export of print should be made after import [David Ichim - [`2a670db`](https://github.com/eea/volto-eea-website-theme/commit/2a670db0a9bcb741ea24ea3ac98ba7267b6a83e1)]
27
+ - test: TokenWidget.test.jsx and TopicsWidget.test.jsx cover more conditions [laszlocseh - [`dd154bb`](https://github.com/eea/volto-eea-website-theme/commit/dd154bb520237f458be563280cac1545eb381bdf)]
28
+ - Bump version to 1.29.0 from 1.28.4 [Claudia Ifrim - [`7bc8eab`](https://github.com/eea/volto-eea-website-theme/commit/7bc8eabd4b0462fc5afc07ed131d074af642cc89)]
29
+ - test: added TokenWidget.test.jsx and TopicsWidget.test.jsx [laszlocseh - [`f7292bb`](https://github.com/eea/volto-eea-website-theme/commit/f7292bb426591f758dcc7bc159b5dbd75b7afb36)]
30
+ - test: added HomePageView.test.jsx and HomePageInverseView.test.jsx [laszlocseh - [`2076651`](https://github.com/eea/volto-eea-website-theme/commit/2076651820693825b5e20b0a7774307cd78eeb57)]
31
+ - test: fix in Logo.test.jsx [laszlocseh - [`6d552f8`](https://github.com/eea/volto-eea-website-theme/commit/6d552f89d5aff6e47ef3032d4862a1bcbe364c01)]
32
+ - test: add Logo.test.jsx [laszlocseh - [`752c562`](https://github.com/eea/volto-eea-website-theme/commit/752c5629840d906382858f088b97d33147684ca8)]
33
+ - add isPrint missing files [laszlocseh - [`d995789`](https://github.com/eea/volto-eea-website-theme/commit/d995789f337a00455d45b5ec26de9c4c0c898ce6)]
34
+ ### [1.29.0](https://github.com/eea/volto-eea-website-theme/compare/1.28.3...1.29.0) - 7 March 2024
35
+
7
36
  ### [1.28.3](https://github.com/eea/volto-eea-website-theme/compare/1.28.2...1.28.3) - 5 March 2024
8
37
 
9
38
  #### :bug: Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "1.28.3",
3
+ "version": "1.30.0",
4
4
  "description": "@eeacms/volto-eea-website-theme: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -1 +1 @@
1
- export * from './schema';
1
+ export * from './print';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Print action.
3
+ * @module actions/print
4
+ */
5
+
6
+ import { SET_ISPRINT } from '@eeacms/volto-eea-website-theme/constants/ActionTypes';
7
+
8
+ export const setIsPrint = (data) => {
9
+ return {
10
+ type: SET_ISPRINT,
11
+ payload: data,
12
+ };
13
+ };
@@ -1,11 +1,11 @@
1
1
  import React, { useCallback, useMemo, useRef } from 'react';
2
2
  import { Helmet } from '@plone/volto/helpers';
3
3
  import { compose } from 'redux';
4
- import { connect } from 'react-redux';
4
+ import { connect, useDispatch } from 'react-redux';
5
5
  import { withRouter } from 'react-router';
6
6
  import { defineMessages, injectIntl } from 'react-intl';
7
7
  import startCase from 'lodash/startCase';
8
- import { Icon } from 'semantic-ui-react';
8
+ import { Icon, Loader } from 'semantic-ui-react';
9
9
  import Popup from '@eeacms/volto-eea-design-system/ui/Popup/Popup';
10
10
  import config from '@plone/volto/registry';
11
11
  import Banner from '@eeacms/volto-eea-design-system/ui/Banner/Banner';
@@ -14,7 +14,8 @@ import {
14
14
  sharePage,
15
15
  } from '@eeacms/volto-eea-design-system/ui/Banner/Banner';
16
16
  import Copyright from '@eeacms/volto-eea-design-system/ui/Copyright/Copyright';
17
-
17
+ import { setIsPrint } from '@eeacms/volto-eea-website-theme/actions/print';
18
+ import cx from 'classnames';
18
19
  import './styles.less';
19
20
 
20
21
  const messages = defineMessages({
@@ -65,6 +66,7 @@ const Title = ({ config = {}, properties }) => {
65
66
  };
66
67
 
67
68
  const View = (props) => {
69
+ const dispatch = useDispatch();
68
70
  const { banner = {}, intl } = props;
69
71
  const metadata = props.metadata || props.properties;
70
72
  const popupRef = useRef(null);
@@ -164,14 +166,95 @@ const View = (props) => {
164
166
  </>
165
167
  )}
166
168
  {!hideDownloadButton && (
167
- <Banner.Action
168
- icon="ri-download-2-fill"
169
- title={intl.formatMessage(messages.download)}
170
- className="download"
171
- onClick={() => {
172
- window.print();
173
- }}
174
- />
169
+ <>
170
+ <Banner.Action
171
+ icon="ri-download-2-fill"
172
+ title={intl.formatMessage(messages.download)}
173
+ className="download"
174
+ onClick={() => {
175
+ // set tabs to be visible
176
+ const tabs = document.getElementsByClassName('ui tab');
177
+ Array.from(tabs).forEach((tab) => {
178
+ tab.style.display = 'block';
179
+ });
180
+
181
+ dispatch(setIsPrint(true));
182
+ // display loader
183
+ const printLoader = document.getElementById(
184
+ 'download-print-loader',
185
+ );
186
+ printLoader.style.display = 'flex';
187
+
188
+ let timeoutValue = 1000;
189
+ // if we have plotlycharts increase timeout
190
+ setTimeout(() => {
191
+ const plotlyCharts = document.getElementsByClassName(
192
+ 'visualization-wrapper',
193
+ );
194
+ if (plotlyCharts.length > 0) {
195
+ timeoutValue = timeoutValue + 1000;
196
+ }
197
+ }, timeoutValue);
198
+
199
+ // scroll to iframes to make them be in the viewport
200
+ // use timeout to wait for load
201
+ setTimeout(() => {
202
+ const iframes = document.getElementsByTagName('iframe');
203
+ if (iframes.length > 0) {
204
+ timeoutValue = timeoutValue + 2000;
205
+ Array.from(iframes).forEach((element, index) => {
206
+ setTimeout(() => {
207
+ element.scrollIntoView({
208
+ behavior: 'instant',
209
+ block: 'nearest',
210
+ inline: 'center',
211
+ });
212
+ }, timeoutValue);
213
+ timeoutValue = timeoutValue + 3000;
214
+ });
215
+ timeoutValue = timeoutValue + 1000;
216
+ }
217
+
218
+ setTimeout(() => {
219
+ window.scrollTo({
220
+ top: 0,
221
+ });
222
+ Array.from(tabs).forEach((tab) => {
223
+ tab.style.display = '';
224
+ });
225
+ printLoader.style.display = 'none';
226
+ dispatch(setIsPrint(false));
227
+ window.print();
228
+ }, timeoutValue);
229
+ }, timeoutValue);
230
+ }}
231
+ />
232
+ <div
233
+ id="download-print-loader"
234
+ className={cx('ui warning message')}
235
+ style={{
236
+ position: 'fixed',
237
+ left: '40%',
238
+ right: '40%',
239
+ backgroundColor: '#fff',
240
+ padding: '1em',
241
+ display: 'none',
242
+ flexDirection: 'column',
243
+ alignItems: 'center',
244
+ top: '40%',
245
+ zIndex: '9999',
246
+ }}
247
+ >
248
+ <Loader
249
+ disabled={false}
250
+ indeterminate
251
+ active
252
+ inline
253
+ size="medium"
254
+ ></Loader>
255
+ <div>Preparing download</div>
256
+ </div>
257
+ </>
175
258
  )}
176
259
  {rssLinks?.map((rssLink, index) => (
177
260
  <>
@@ -253,6 +336,7 @@ export default compose(
253
336
  connect((state) => {
254
337
  return {
255
338
  types: state.types.types,
339
+ isPrint: state.isPrint,
256
340
  };
257
341
  }),
258
342
  )(View);
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import HomePageInverseView from './HomePageInverseView';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+
6
+ describe('HomePageInverseView Component', () => {
7
+ it('renders without crashing', () => {
8
+ const mockContent = (
9
+ <body>
10
+ <div>Mock content</div>
11
+ </body>
12
+ );
13
+ const { container } = render(<HomePageInverseView content={mockContent} />);
14
+ expect(container).toBeTruthy();
15
+ });
16
+ });
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import HomePageView from './HomePageView';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+
6
+ describe('HomePageView Component', () => {
7
+ it('renders without crashing', () => {
8
+ const mockContent = (
9
+ <body>
10
+ <div>Mock content</div>
11
+ </body>
12
+ );
13
+ const { container } = render(<HomePageView content={mockContent} />);
14
+ expect(container).toBeTruthy();
15
+ });
16
+ });
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { Provider } from 'react-intl-redux';
4
+ import configureStore from 'redux-mock-store';
5
+ import { Router } from 'react-router-dom';
6
+ import { createMemoryHistory } from 'history';
7
+ import EEALogo from './Logo';
8
+
9
+ const mockStore = configureStore();
10
+ let history = createMemoryHistory();
11
+
12
+ describe('EEALogo Component', () => {
13
+ it('renders without crashing', () => {
14
+ const store = mockStore({
15
+ intl: {
16
+ locale: 'en',
17
+ messages: {},
18
+ },
19
+ });
20
+
21
+ const { container } = render(
22
+ <Provider store={store}>
23
+ <Router history={history}>
24
+ <EEALogo />
25
+ </Router>
26
+ </Provider>,
27
+ );
28
+
29
+ expect(container).toBeTruthy();
30
+ });
31
+ });
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { Provider } from 'react-intl-redux';
4
+ import configureStore from 'redux-mock-store';
5
+ import { Router } from 'react-router-dom';
6
+ import { createMemoryHistory } from 'history';
7
+ import { TokenWidget } from './TokenWidget';
8
+
9
+ const mockStore = configureStore();
10
+ let history = createMemoryHistory();
11
+
12
+ describe('TokenWidget Component', () => {
13
+ it('renders without crashing', () => {
14
+ const store = mockStore({
15
+ intl: {
16
+ locale: 'en',
17
+ messages: {},
18
+ },
19
+ });
20
+
21
+ const { container } = render(
22
+ <Provider store={store}>
23
+ <Router history={history}>
24
+ <TokenWidget
25
+ value={['Value1', 'Value2']}
26
+ children={''}
27
+ className={'test'}
28
+ />
29
+ </Router>
30
+ </Provider>,
31
+ );
32
+
33
+ expect(container).toBeTruthy();
34
+ });
35
+
36
+ it('renders without crashing, without value', () => {
37
+ const store = mockStore({
38
+ intl: {
39
+ locale: 'en',
40
+ messages: {},
41
+ },
42
+ });
43
+
44
+ const { container } = render(
45
+ <Provider store={store}>
46
+ <Router history={history}>
47
+ <TokenWidget value={null} children={''} className={'test'} />
48
+ </Router>
49
+ </Provider>,
50
+ );
51
+
52
+ expect(container).toBeTruthy();
53
+ });
54
+ });
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { Provider } from 'react-intl-redux';
4
+ import configureStore from 'redux-mock-store';
5
+ import { Router } from 'react-router-dom';
6
+ import { createMemoryHistory } from 'history';
7
+ import { TopicsWidget } from './TopicsWidget';
8
+
9
+ const mockStore = configureStore();
10
+ let history = createMemoryHistory();
11
+
12
+ describe('TopicsWidget Component', () => {
13
+ it('renders without crashing, with value', () => {
14
+ const store = mockStore({
15
+ intl: {
16
+ locale: 'en',
17
+ messages: {},
18
+ },
19
+ });
20
+
21
+ const { container } = render(
22
+ <Provider store={store}>
23
+ <Router history={history}>
24
+ <TopicsWidget
25
+ value={['Value1', 'Value2']}
26
+ children={''}
27
+ className={'test'}
28
+ />
29
+ </Router>
30
+ </Provider>,
31
+ );
32
+
33
+ expect(container).toBeTruthy();
34
+ });
35
+
36
+ it('renders without crashing, without value', () => {
37
+ const store = mockStore({
38
+ intl: {
39
+ locale: 'en',
40
+ messages: {},
41
+ },
42
+ });
43
+
44
+ const { container } = render(
45
+ <Provider store={store}>
46
+ <Router history={history}>
47
+ <TopicsWidget value={null} children={''} className={'test'} />
48
+ </Router>
49
+ </Provider>,
50
+ );
51
+
52
+ expect(container).toBeTruthy();
53
+ });
54
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Action types.
3
+ * @module constants/ActionTypes
4
+ */
5
+
6
+ export const SET_ISPRINT = 'SET_ISPRINT';
@@ -20,7 +20,7 @@ import { Copyright } from '@eeacms/volto-eea-design-system/ui';
20
20
  */
21
21
  export const View = (props) => {
22
22
  const { data, detached } = props;
23
- const href = data?.href?.[0]?.['@id'] || '';
23
+ const href = data?.href?.[0]?.['@id'] ?? (data?.href || '');
24
24
  const { copyright, copyrightIcon, copyrightPosition } = data;
25
25
  // const [hovering, setHovering] = React.useState(false);
26
26
  const [viewLoaded, setViewLoaded] = React.useState(false);
@@ -0,0 +1,106 @@
1
+ //this customization is used for fixing: in the search block, on edit sort on and reversed order doesn't work.
2
+ //See here volto pr: https://github.com/plone/volto/pull/5262
3
+ import React, { useEffect } from 'react';
4
+ import { defineMessages } from 'react-intl';
5
+ import { compose } from 'redux';
6
+
7
+ import { SidebarPortal, BlockDataForm } from '@plone/volto/components';
8
+ import { addExtensionFieldToSchema } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
9
+ import { getBaseUrl } from '@plone/volto/helpers';
10
+ import config from '@plone/volto/registry';
11
+
12
+ import { SearchBlockViewComponent } from '@plone/volto/components/manage/Blocks/Search/SearchBlockView';
13
+ import Schema from '@plone/volto/components/manage/Blocks/Search/schema';
14
+ import {
15
+ withSearch,
16
+ withQueryString,
17
+ } from '@plone/volto/components/manage/Blocks/Search/hocs';
18
+ import { cloneDeep } from 'lodash';
19
+
20
+ const messages = defineMessages({
21
+ template: {
22
+ id: 'Results template',
23
+ defaultMessage: 'Results template',
24
+ },
25
+ });
26
+
27
+ const SearchBlockEdit = (props) => {
28
+ const {
29
+ block,
30
+ onChangeBlock,
31
+ data,
32
+ selected,
33
+ intl,
34
+ navRoot,
35
+ contentType,
36
+ onTriggerSearch,
37
+ querystring = {},
38
+ } = props;
39
+ const { sortable_indexes = {} } = querystring;
40
+
41
+ let schema = Schema({ data, intl });
42
+
43
+ schema = addExtensionFieldToSchema({
44
+ schema,
45
+ name: 'listingBodyTemplate',
46
+ items: config.blocks.blocksConfig.listing.variations,
47
+ intl,
48
+ title: { id: intl.formatMessage(messages.template) },
49
+ });
50
+ const listingVariations = config.blocks.blocksConfig?.listing?.variations;
51
+ let activeItem = listingVariations.find(
52
+ (item) => item.id === data.listingBodyTemplate,
53
+ );
54
+ const listingSchemaEnhancer = activeItem?.schemaEnhancer;
55
+ if (listingSchemaEnhancer)
56
+ schema = listingSchemaEnhancer({
57
+ schema: cloneDeep(schema),
58
+ data,
59
+ intl,
60
+ });
61
+ schema.properties.sortOnOptions.items = {
62
+ choices: Object.keys(sortable_indexes).map((k) => [
63
+ k,
64
+ sortable_indexes[k].title,
65
+ ]),
66
+ };
67
+
68
+ const { query = {} } = data || {};
69
+ // We don't need deep compare here, as this is just json serializable data.
70
+ const deepQuery = JSON.stringify(query);
71
+ useEffect(() => {
72
+ onTriggerSearch(
73
+ '',
74
+ data?.facets,
75
+ data?.query?.sort_on,
76
+ data?.query?.sort_order,
77
+ );
78
+ }, [deepQuery, onTriggerSearch, data]);
79
+
80
+ return (
81
+ <>
82
+ <SearchBlockViewComponent
83
+ {...props}
84
+ path={getBaseUrl(props.pathname)}
85
+ mode="edit"
86
+ />
87
+ <SidebarPortal selected={selected}>
88
+ <BlockDataForm
89
+ schema={schema}
90
+ onChangeField={(id, value) => {
91
+ onChangeBlock(block, {
92
+ ...data,
93
+ [id]: value,
94
+ });
95
+ }}
96
+ onChangeBlock={onChangeBlock}
97
+ formData={data}
98
+ navRoot={navRoot}
99
+ contentType={contentType}
100
+ />
101
+ </SidebarPortal>
102
+ </>
103
+ );
104
+ };
105
+
106
+ export default compose(withQueryString, withSearch())(SearchBlockEdit);
@@ -0,0 +1,479 @@
1
+ //this customization is used for fixing: in the search block, on edit sort on and reversed order doesn't work.
2
+ //See here volto pr: https://github.com/plone/volto/pull/5262
3
+ import React from 'react';
4
+ import { useSelector } from 'react-redux';
5
+ import qs from 'query-string';
6
+ import { useLocation, useHistory } from 'react-router-dom';
7
+
8
+ import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
9
+ import config from '@plone/volto/registry';
10
+ import { usePrevious } from '@plone/volto/helpers';
11
+ import { isEqual } from 'lodash';
12
+
13
+ function getDisplayName(WrappedComponent) {
14
+ return WrappedComponent.displayName || WrappedComponent.name || 'Component';
15
+ }
16
+
17
+ const SEARCH_ENDPOINT_FIELDS = [
18
+ 'SearchableText',
19
+ 'b_size',
20
+ 'limit',
21
+ 'sort_on',
22
+ 'sort_order',
23
+ ];
24
+
25
+ const PAQO = 'plone.app.querystring.operation';
26
+
27
+ /**
28
+ * Based on URL state, gets an initial internal state for the search
29
+ *
30
+ * @function getInitialState
31
+ *
32
+ */
33
+ function getInitialState(
34
+ data,
35
+ facets,
36
+ urlSearchText,
37
+ id,
38
+ sortOnParam,
39
+ sortOrderParam,
40
+ ) {
41
+ const {
42
+ types: facetWidgetTypes,
43
+ } = config.blocks.blocksConfig.search.extensions.facetWidgets;
44
+ const facetSettings = data?.facets || [];
45
+
46
+ return {
47
+ query: [
48
+ ...(data.query?.query || []),
49
+ ...(facetSettings || [])
50
+ .map((facet) => {
51
+ if (!facet?.field) return null;
52
+
53
+ const { valueToQuery } = resolveExtension(
54
+ 'type',
55
+ facetWidgetTypes,
56
+ facet,
57
+ );
58
+
59
+ const name = facet.field.value;
60
+ const value = facets[name];
61
+
62
+ return valueToQuery({ value, facet });
63
+ })
64
+ .filter((f) => !!f),
65
+ ...(urlSearchText
66
+ ? [
67
+ {
68
+ i: 'SearchableText',
69
+ o: 'plone.app.querystring.operation.string.contains',
70
+ v: urlSearchText,
71
+ },
72
+ ]
73
+ : []),
74
+ ],
75
+ sort_on: sortOnParam || data.query?.sort_on,
76
+ sort_order: sortOrderParam || data.query?.sort_order,
77
+ b_size: data.query?.b_size,
78
+ limit: data.query?.limit,
79
+ block: id,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * "Normalizes" the search state to something that's serializable
85
+ * (for querying) and used to compute data for the ListingBody
86
+ *
87
+ * @function normalizeState
88
+ *
89
+ */
90
+ function normalizeState({
91
+ query, // base query
92
+ facets, // facet values
93
+ id, // block id
94
+ searchText, // SearchableText
95
+ sortOn,
96
+ sortOrder,
97
+ facetSettings, // data.facets extracted from block data
98
+ }) {
99
+ const {
100
+ types: facetWidgetTypes,
101
+ } = config.blocks.blocksConfig.search.extensions.facetWidgets;
102
+
103
+ // Here, we are removing the QueryString of the Listing ones, which is present in the Facet
104
+ // because we already initialize the facet with those values.
105
+ const configuredFacets = facetSettings
106
+ ? facetSettings.map((facet) => facet?.field?.value)
107
+ : [];
108
+
109
+ let copyOfQuery = query.query ? [...query.query] : [];
110
+
111
+ const queryWithoutFacet = copyOfQuery.filter((query) => {
112
+ return !configuredFacets.includes(query.i);
113
+ });
114
+
115
+ const params = {
116
+ query: [
117
+ ...(queryWithoutFacet || []),
118
+ ...(facetSettings || []).map((facet) => {
119
+ if (!facet?.field) return null;
120
+
121
+ const { valueToQuery } = resolveExtension(
122
+ 'type',
123
+ facetWidgetTypes,
124
+ facet,
125
+ );
126
+
127
+ const name = facet.field.value;
128
+ const value = facets[name];
129
+
130
+ return valueToQuery({ value, facet });
131
+ }),
132
+ ].filter((o) => !!o),
133
+ sort_on: sortOn || query.sort_on,
134
+ sort_order: sortOrder || query.sort_order,
135
+ b_size: query.b_size,
136
+ limit: query.limit,
137
+ block: id,
138
+ };
139
+
140
+ // Note Ideally the searchtext functionality should be restructured as being just
141
+ // another facet. But right now it's the same. This means that if a searchText
142
+ // is provided, it will override the SearchableText facet.
143
+ // If there is no searchText, the SearchableText in the query remains in effect.
144
+ // TODO eventually the searchText should be a distinct facet from SearchableText, and
145
+ // the two conditions could be combined, in comparison to the current state, when
146
+ // one overrides the other.
147
+ if (searchText) {
148
+ params.query = params.query.reduce(
149
+ // Remove SearchableText from query
150
+ (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
151
+ [],
152
+ );
153
+ params.query.push({
154
+ i: 'SearchableText',
155
+ o: 'plone.app.querystring.operation.string.contains',
156
+ v: searchText,
157
+ });
158
+ }
159
+
160
+ return params;
161
+ }
162
+
163
+ const getSearchFields = (searchData) => {
164
+ return Object.assign(
165
+ {},
166
+ ...SEARCH_ENDPOINT_FIELDS.map((k) => {
167
+ return searchData[k] ? { [k]: searchData[k] } : {};
168
+ }),
169
+ searchData.query ? { query: serializeQuery(searchData['query']) } : {},
170
+ );
171
+ };
172
+
173
+ /**
174
+ * A hook that will mirror the search block state to a hash location
175
+ */
176
+ const useHashState = () => {
177
+ const location = useLocation();
178
+ const history = useHistory();
179
+
180
+ /**
181
+ * Required to maintain parameter compatibility.
182
+ With this we will maintain support for receiving hash (#) and search (?) type parameters.
183
+ */
184
+ const oldState = React.useMemo(() => {
185
+ return {
186
+ ...qs.parse(location.search),
187
+ ...qs.parse(location.hash),
188
+ };
189
+ }, [location.hash, location.search]);
190
+
191
+ // This creates a shallow copy. Why is this needed?
192
+ const current = Object.assign(
193
+ {},
194
+ ...Array.from(Object.keys(oldState)).map((k) => ({ [k]: oldState[k] })),
195
+ );
196
+
197
+ const setSearchData = React.useCallback(
198
+ (searchData) => {
199
+ const newParams = qs.parse(location.search);
200
+
201
+ let changed = false;
202
+
203
+ Object.keys(searchData)
204
+ .sort()
205
+ .forEach((k) => {
206
+ if (searchData[k]) {
207
+ newParams[k] = searchData[k];
208
+ if (oldState[k] !== searchData[k]) {
209
+ changed = true;
210
+ }
211
+ }
212
+ });
213
+
214
+ if (changed) {
215
+ history.push({
216
+ search: qs.stringify(newParams),
217
+ });
218
+ }
219
+ },
220
+ [history, oldState, location.search],
221
+ );
222
+
223
+ return [current, setSearchData];
224
+ };
225
+
226
+ /**
227
+ * A hook to make it possible to switch disable mirroring the search block
228
+ * state to the window location. When using the internal state we "start from
229
+ * scratch", as it's intended to be used in the edit page.
230
+ */
231
+ const useSearchBlockState = (uniqueId, isEditMode) => {
232
+ const [hashState, setHashState] = useHashState();
233
+ const [internalState, setInternalState] = React.useState({});
234
+
235
+ return isEditMode
236
+ ? [internalState, setInternalState]
237
+ : [hashState, setHashState];
238
+ };
239
+
240
+ // Simple compress/decompress the state in URL by replacing the lengthy string
241
+ const deserializeQuery = (q) => {
242
+ return JSON.parse(q)?.map((kvp) => ({
243
+ ...kvp,
244
+ o: kvp.o.replace(/^paqo/, PAQO),
245
+ }));
246
+ };
247
+ const serializeQuery = (q) => {
248
+ return JSON.stringify(
249
+ q?.map((kvp) => ({ ...kvp, o: kvp.o.replace(PAQO, 'paqo') })),
250
+ );
251
+ };
252
+
253
+ const withSearch = (options) => (WrappedComponent) => {
254
+ const { inputDelay = 1000 } = options || {};
255
+
256
+ function WithSearch(props) {
257
+ const { data, id, editable = false } = props;
258
+
259
+ const [locationSearchData, setLocationSearchData] = useSearchBlockState(
260
+ id,
261
+ editable,
262
+ );
263
+
264
+ // TODO: Improve the hook dependencies out of the scope of https://github.com/plone/volto/pull/4662
265
+ // eslint-disable-next-line react-hooks/exhaustive-deps
266
+ const urlQuery = locationSearchData.query
267
+ ? deserializeQuery(locationSearchData.query)
268
+ : [];
269
+ const urlSearchText =
270
+ locationSearchData.SearchableText ||
271
+ urlQuery.find(({ i }) => i === 'SearchableText')?.v ||
272
+ '';
273
+
274
+ // TODO: refactor, should use only useLocationStateManager()!!!
275
+ const [searchText, setSearchText] = React.useState(urlSearchText);
276
+ // TODO: Improve the hook dependencies out of the scope of https://github.com/plone/volto/pull/4662
277
+ // eslint-disable-next-line react-hooks/exhaustive-deps
278
+ const configuredFacets =
279
+ data.facets?.map((facet) => facet?.field?.value) || [];
280
+
281
+ // Here we are getting the initial value of the facet if Listing Query contains the same criteria as
282
+ // facet.
283
+ const queryData = data?.query?.query
284
+ ? deserializeQuery(JSON.stringify(data?.query?.query))
285
+ : [];
286
+
287
+ let intializeFacetWithQueryValue = [];
288
+
289
+ for (let value of configuredFacets) {
290
+ const queryString = queryData.find((item) => item.i === value);
291
+ if (queryString) {
292
+ intializeFacetWithQueryValue = [
293
+ ...intializeFacetWithQueryValue,
294
+ { [queryString.i]: queryString.v },
295
+ ];
296
+ }
297
+ }
298
+
299
+ const multiFacets = data.facets
300
+ ?.filter((facet) => facet?.multiple)
301
+ .map((facet) => facet?.field?.value);
302
+ const [facets, setFacets] = React.useState(
303
+ Object.assign(
304
+ {},
305
+ ...urlQuery.map(({ i, v }) => ({ [i]: v })),
306
+ // TODO: the 'o' should be kept. This would be a major refactoring of the facets
307
+ ...intializeFacetWithQueryValue,
308
+ // support for simple filters like ?Subject=something
309
+ // TODO: since the move to hash params this is no longer working.
310
+ // We'd have to treat the location.search and manage it just like the
311
+ // hash, to support it. We can read it, but we'd have to reset it as
312
+ // well, so at that point what's the difference to the hash?
313
+ ...configuredFacets.map((f) =>
314
+ locationSearchData[f]
315
+ ? {
316
+ [f]:
317
+ multiFacets.indexOf(f) > -1
318
+ ? [locationSearchData[f]]
319
+ : locationSearchData[f],
320
+ }
321
+ : {},
322
+ ),
323
+ ),
324
+ );
325
+ const previousUrlQuery = usePrevious(urlQuery);
326
+
327
+ // During first render the previousUrlQuery is undefined and urlQuery
328
+ // is empty so it ressetting the facet when you are navigating but during reload we have urlQuery and we need
329
+ // to set the facet at first render.
330
+ const preventOverrideOfFacetState =
331
+ previousUrlQuery === undefined && urlQuery.length === 0;
332
+
333
+ React.useEffect(() => {
334
+ if (
335
+ !isEqual(urlQuery, previousUrlQuery) &&
336
+ !preventOverrideOfFacetState
337
+ ) {
338
+ setFacets(
339
+ Object.assign(
340
+ {},
341
+ ...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
342
+
343
+ // support for simple filters like ?Subject=something
344
+ // TODO: since the move to hash params this is no longer working.
345
+ // We'd have to treat the location.search and manage it just like the
346
+ // hash, to support it. We can read it, but we'd have to reset it as
347
+ // well, so at that point what's the difference to the hash?
348
+ ...configuredFacets.map((f) =>
349
+ locationSearchData[f]
350
+ ? {
351
+ [f]:
352
+ multiFacets.indexOf(f) > -1
353
+ ? [locationSearchData[f]]
354
+ : locationSearchData[f],
355
+ }
356
+ : {},
357
+ ),
358
+ ),
359
+ );
360
+ }
361
+ }, [
362
+ urlQuery,
363
+ configuredFacets,
364
+ locationSearchData,
365
+ multiFacets,
366
+ previousUrlQuery,
367
+ preventOverrideOfFacetState,
368
+ ]);
369
+
370
+ const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
371
+ const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
372
+
373
+ const [searchData, setSearchData] = React.useState(
374
+ getInitialState(data, facets, urlSearchText, id),
375
+ );
376
+
377
+ const deepFacets = JSON.stringify(facets);
378
+ const deepData = JSON.stringify(data);
379
+ React.useEffect(() => {
380
+ setSearchData(
381
+ getInitialState(
382
+ JSON.parse(deepData),
383
+ JSON.parse(deepFacets),
384
+ urlSearchText,
385
+ id,
386
+ sortOn,
387
+ sortOrder,
388
+ ),
389
+ );
390
+ }, [deepData, deepFacets, urlSearchText, id, sortOn, sortOrder]);
391
+
392
+ const timeoutRef = React.useRef();
393
+ const facetSettings = data?.facets;
394
+
395
+ const deepQuery = JSON.stringify(data.query);
396
+ const onTriggerSearch = React.useCallback(
397
+ (
398
+ toSearchText = undefined,
399
+ toSearchFacets = undefined,
400
+ toSortOn = undefined,
401
+ toSortOrder = undefined,
402
+ ) => {
403
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
404
+ timeoutRef.current = setTimeout(
405
+ () => {
406
+ const newSearchData = normalizeState({
407
+ id,
408
+ query: data.query || {},
409
+ facets: toSearchFacets || facets,
410
+ searchText: toSearchText ? toSearchText.trim() : '',
411
+ sortOn: toSortOn || undefined,
412
+ sortOrder: toSortOrder || sortOrder,
413
+ facetSettings,
414
+ });
415
+ if (toSearchFacets) setFacets(toSearchFacets);
416
+ if (toSortOn) setSortOn(toSortOn || undefined);
417
+ if (toSortOrder) setSortOrder(toSortOrder);
418
+ setSearchData(newSearchData);
419
+ setLocationSearchData(getSearchFields(newSearchData));
420
+ },
421
+ toSearchFacets ? inputDelay / 3 : inputDelay,
422
+ );
423
+ },
424
+ // eslint-disable-next-line react-hooks/exhaustive-deps
425
+ [
426
+ // Use deep comparison of data.query
427
+ deepQuery,
428
+ facets,
429
+ id,
430
+ setLocationSearchData,
431
+ searchText,
432
+ sortOn,
433
+ sortOrder,
434
+ facetSettings,
435
+ ],
436
+ );
437
+
438
+ const removeSearchQuery = () => {
439
+ let newSearchData = { ...searchData };
440
+ newSearchData.query = searchData.query.reduce(
441
+ // Remove SearchableText from query
442
+ (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
443
+ [],
444
+ );
445
+ setSearchData(newSearchData);
446
+ setLocationSearchData(getSearchFields(newSearchData));
447
+ };
448
+
449
+ const querystringResults = useSelector(
450
+ (state) => state.querystringsearch.subrequests,
451
+ );
452
+ const totalItems =
453
+ querystringResults[id]?.total || querystringResults[id]?.items?.length;
454
+
455
+ return (
456
+ <WrappedComponent
457
+ {...props}
458
+ searchData={searchData}
459
+ facets={facets}
460
+ setFacets={setFacets}
461
+ setSortOn={setSortOn}
462
+ setSortOrder={setSortOrder}
463
+ sortOn={sortOn}
464
+ sortOrder={sortOrder}
465
+ searchedText={urlSearchText}
466
+ searchText={searchText}
467
+ removeSearchQuery={removeSearchQuery}
468
+ setSearchText={setSearchText}
469
+ onTriggerSearch={onTriggerSearch}
470
+ totalItems={totalItems}
471
+ />
472
+ );
473
+ }
474
+ WithSearch.displayName = `WithSearch(${getDisplayName(WrappedComponent)})`;
475
+
476
+ return WithSearch;
477
+ };
478
+
479
+ export default withSearch;
@@ -0,0 +1,154 @@
1
+ import React from 'react';
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Segment, Header, List } from 'semantic-ui-react';
4
+ import {
5
+ When,
6
+ Recurrence,
7
+ } from '@plone/volto/components/theme/View/EventDatesInfo';
8
+ import { Icon } from '@plone/volto/components';
9
+ import { expandToBackendURL } from '@plone/volto/helpers';
10
+
11
+ import calendarSVG from '@plone/volto/icons/calendar.svg';
12
+
13
+ const messages = defineMessages({
14
+ what: {
15
+ id: 'event_what',
16
+ defaultMessage: 'What',
17
+ },
18
+ when: {
19
+ id: 'event_when',
20
+ defaultMessage: 'When',
21
+ },
22
+ allDates: {
23
+ id: 'event_alldates',
24
+ defaultMessage: 'All dates',
25
+ },
26
+ downloadEvent: {
27
+ id: 'Download Event',
28
+ defaultMessage: 'Download Event',
29
+ },
30
+ where: {
31
+ id: 'event_where',
32
+ defaultMessage: 'Where',
33
+ },
34
+ contactName: {
35
+ id: 'event_contactname',
36
+ defaultMessage: 'Contact Name',
37
+ },
38
+ contactPhone: {
39
+ id: 'event_contactphone',
40
+ defaultMessage: 'Contact Phone',
41
+ },
42
+ attendees: {
43
+ id: 'event_attendees',
44
+ defaultMessage: 'Attendees',
45
+ },
46
+ website: {
47
+ id: 'event_website',
48
+ defaultMessage: 'Website',
49
+ },
50
+ visitWebsite: {
51
+ id: 'visit_external_website',
52
+ defaultMessage: 'Visit external website',
53
+ },
54
+ });
55
+
56
+ const EventDetails = ({ content, display_as = 'aside' }) => {
57
+ const intl = useIntl();
58
+ return (
59
+ <Segment
60
+ as={display_as}
61
+ {...(display_as === 'aside' ? { floated: 'right' } : {})}
62
+ >
63
+ {content.subjects?.length > 0 && (
64
+ <>
65
+ <Header dividing sub>
66
+ {intl.formatMessage(messages.what)}
67
+ </Header>
68
+ <List items={content.subjects} />
69
+ </>
70
+ )}
71
+ <Header dividing sub>
72
+ {intl.formatMessage(messages.when)}
73
+ </Header>
74
+ <When
75
+ start={content.start}
76
+ end={content.end}
77
+ whole_day={content.whole_day}
78
+ open_end={content.open_end}
79
+ />
80
+ {content.recurrence && (
81
+ <>
82
+ <Header dividing sub>
83
+ {intl.formatMessage(messages.allDates)}
84
+ </Header>
85
+ <Recurrence recurrence={content.recurrence} start={content.start} />
86
+ </>
87
+ )}
88
+ {content.location && (
89
+ <>
90
+ <Header dividing sub>
91
+ {intl.formatMessage(messages.where)}
92
+ </Header>
93
+ <p>{content.location}</p>
94
+ </>
95
+ )}
96
+ {content.contact_name && (
97
+ <>
98
+ <Header dividing sub>
99
+ {intl.formatMessage(messages.contactName)}
100
+ </Header>
101
+ <p>
102
+ {content.contact_email ? (
103
+ <a href={`mailto:${content.contact_email}`}>
104
+ {content.contact_name}
105
+ </a>
106
+ ) : (
107
+ content.contact_name
108
+ )}
109
+ </p>
110
+ </>
111
+ )}
112
+ {content.contact_phone && (
113
+ <>
114
+ <Header dividing sub>
115
+ {intl.formatMessage(messages.contactPhone)}
116
+ </Header>
117
+ <p>{content.contact_phone}</p>
118
+ </>
119
+ )}
120
+ {content.attendees?.length > 0 && (
121
+ <>
122
+ <Header dividing sub>
123
+ {intl.formatMessage(messages.attendees)}
124
+ </Header>
125
+ <List items={content.attendees} />
126
+ </>
127
+ )}
128
+ {content.event_url && (
129
+ <>
130
+ <Header dividing sub>
131
+ {intl.formatMessage(messages.website)}
132
+ </Header>
133
+ <p>
134
+ <a href={content.event_url} target="_blank" rel="noopener">
135
+ {intl.formatMessage(messages.visitWebsite)}
136
+ </a>
137
+ </p>
138
+ </>
139
+ )}
140
+ <div className="download-event">
141
+ <Icon name={calendarSVG} />
142
+ <a
143
+ className="ics-download"
144
+ target="_blank"
145
+ href={`${expandToBackendURL(content['@id'])}/ics_view`}
146
+ >
147
+ {intl.formatMessage(messages.downloadEvent)}
148
+ </a>
149
+ </div>
150
+ </Segment>
151
+ );
152
+ };
153
+
154
+ export default EventDetails;
package/src/index.js CHANGED
@@ -27,7 +27,7 @@ import contentBoxSVG from './icons/content-box.svg';
27
27
  import okMiddleware from './middleware/ok';
28
28
  import voltoCustomMiddleware from './middleware/voltoCustom';
29
29
  import installSlate from './slate';
30
-
30
+ import { print } from './reducers';
31
31
  import { nanoid } from '@plone/volto-slate/utils';
32
32
  import { v4 as uuid } from 'uuid';
33
33
 
@@ -534,6 +534,12 @@ const applyConfig = (config) => {
534
534
  };
535
535
  }
536
536
 
537
+ // addonReducers
538
+ config.addonReducers = {
539
+ ...(config.addonReducers || {}),
540
+ print,
541
+ };
542
+
537
543
  // Breadcrumbs
538
544
  config.settings.apiExpanders.push({
539
545
  match: '',
package/src/index.test.js CHANGED
@@ -215,6 +215,11 @@ describe('applyConfig', () => {
215
215
  title: 'Horizontal',
216
216
  isDefault: false,
217
217
  },
218
+ {
219
+ id: 'accordion',
220
+ title: 'Accordion responsive',
221
+ isDefault: false,
222
+ },
218
223
  ],
219
224
  },
220
225
  columnsBlock: {},
@@ -0,0 +1,3 @@
1
+ import print from './print';
2
+
3
+ export { print };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Print reducer.
3
+ * @module reducers/print
4
+ */
5
+
6
+ import { SET_ISPRINT } from '@eeacms/volto-eea-website-theme/constants/ActionTypes';
7
+
8
+ const initialState = {
9
+ isPrint: false,
10
+ };
11
+
12
+ export default function print(state = initialState, action) {
13
+ if (action.type === SET_ISPRINT) {
14
+ return {
15
+ ...state,
16
+ isPrint: action.payload,
17
+ };
18
+ } else {
19
+ return state;
20
+ }
21
+ }
@@ -1,5 +0,0 @@
1
- export function removeSchema() {
2
- return {
3
- type: 'REMOVE_SCHEMA',
4
- };
5
- }