@eeacms/volto-eea-website-theme 1.31.0 → 1.32.1

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,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.32.1](https://github.com/eea/volto-eea-website-theme/compare/1.32.0...1.32.1) - 26 March 2024
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(slate): add new h3 slate element [Miu Razvan - [`20a7e93`](https://github.com/eea/volto-eea-website-theme/commit/20a7e9316d5c1de279712c38bc05eb775f8bd326)]
12
+ - feat(slate): add h4 in slate toolbar [Miu Razvan - [`2c523ac`](https://github.com/eea/volto-eea-website-theme/commit/2c523acdeb78ed224c4a37285c48dbf4555604e0)]
13
+
14
+ #### :bug: Bug Fixes
15
+
16
+ - fix(slate): insert/remove element fix, refs #261770 [Miu Razvan - [`b121ffd`](https://github.com/eea/volto-eea-website-theme/commit/b121ffd2de5c5973f9819b949be1c83400d72edb)]
17
+
18
+ #### :nail_care: Enhancements
19
+
20
+ - change(slate): Use F as icon and renamed title of new heading Slate toolbar button to Figure title [David Ichim - [`7355bb3`](https://github.com/eea/volto-eea-website-theme/commit/7355bb3f3cf527c24fc41a10f72cc854773ab84b)]
21
+ - change(slate): renamed addFormat to addSlateToolbarButton [ichim-david - [`0809c04`](https://github.com/eea/volto-eea-website-theme/commit/0809c040ea9e096c60cbce36e679f9bab23a52a2)]
22
+
23
+ #### :house: Internal changes
24
+
25
+ - style: Automated code fix [eea-jenkins - [`6971349`](https://github.com/eea/volto-eea-website-theme/commit/69713496d601e4e0b2fe3469a4ffcd8d36105040)]
26
+ - style: Automated code fix [eea-jenkins - [`a292700`](https://github.com/eea/volto-eea-website-theme/commit/a292700cd047bad925add1fde4d834f7fd03ec62)]
27
+
28
+ #### :hammer_and_wrench: Others
29
+
30
+ - fix conflicts [Miu Razvan - [`9ad80fc`](https://github.com/eea/volto-eea-website-theme/commit/9ad80fc98a3057d96f573150456bc43b5f822782)]
31
+ - fix conflicts [Miu Razvan - [`dee94db`](https://github.com/eea/volto-eea-website-theme/commit/dee94db33c47a2d963ea912712723768de547719)]
32
+ - Update package.json [ichim-david - [`9db96ff`](https://github.com/eea/volto-eea-website-theme/commit/9db96ff3f8e8eedce92c53befe6a1d6e965d8699)]
33
+ - Revert "(fix): In search block on edit, the sort on and sort order are not working (#205)" [David Ichim - [`2045d50`](https://github.com/eea/volto-eea-website-theme/commit/2045d50b3d2f18fab0d6c6794d8030975b1a3f21)]
34
+ ### [1.32.0](https://github.com/eea/volto-eea-website-theme/compare/1.31.0...1.32.0) - 25 March 2024
35
+
7
36
  ### [1.31.0](https://github.com/eea/volto-eea-website-theme/compare/1.30.0...1.31.0) - 14 March 2024
8
37
 
9
38
  #### :rocket: New Features
package/README.md CHANGED
@@ -27,6 +27,8 @@ See [Storybook](https://eea.github.io/eea-storybook/).
27
27
 
28
28
  ## Volto customizations
29
29
 
30
+ - `volto-slate/elementEditor/utils` -> https://github.com/plone/volto/pull/5926
31
+
30
32
  - `volto-slate/editor/SlateEditor` -> When two slates looks at the same prop changing one slate and updating the other should be handled properly. This change makes replacing the old value of slate work in sync with the other slates that watches the same prop [ref](https://taskman.eionet.europa.eu/issues/264239#note-11).
31
33
 
32
34
  **!!IMPORTANT**: This change requires volto@^16.26.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "1.31.0",
3
+ "version": "1.32.1",
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",
@@ -79,4 +79,4 @@
79
79
  "cypress:open": "make cypress-open",
80
80
  "prepare": "husky install"
81
81
  }
82
- }
82
+ }
@@ -0,0 +1,245 @@
1
+ import { Editor, Transforms, Node } from 'slate';
2
+
3
+ /**
4
+ * @description Creates or updates an existing $elementType. It also takes care
5
+ * of the saved selection and uses PathRef.
6
+ *
7
+ * @param {Editor} editor The Slate editor for the context
8
+ * @param {object} data Relevant data for this element
9
+ *
10
+ * @returns {boolean} true if an element was possibly inserted, false otherwise
11
+ * (currently we do not check here if the element was already applied to the
12
+ * editor)
13
+ */
14
+ export const _insertElement = (elementType) => (editor, data) => {
15
+ if (editor.getSavedSelection()) {
16
+ const selection = editor.selection || editor.getSavedSelection();
17
+
18
+ const rangeRef = Editor.rangeRef(editor, selection);
19
+
20
+ const res = Array.from(
21
+ Editor.nodes(editor, {
22
+ match: (n) => n.type === elementType,
23
+ mode: 'highest',
24
+ at: selection,
25
+ }),
26
+ );
27
+
28
+ if (res.length) {
29
+ const [, path] = res[0];
30
+ Transforms.setNodes(
31
+ editor,
32
+ { data },
33
+ {
34
+ at: path ? path : null,
35
+ match: path ? (n) => n.type === elementType : null,
36
+ },
37
+ );
38
+ } else {
39
+ Transforms.wrapNodes(
40
+ editor,
41
+ { type: elementType, data },
42
+ {
43
+ split: true,
44
+ at: selection,
45
+ match: (node) => {
46
+ return Node.string(node).length !== 0;
47
+ },
48
+ },
49
+ );
50
+ }
51
+
52
+ const sel = JSON.parse(JSON.stringify(rangeRef.current));
53
+
54
+ setTimeout(() => {
55
+ Transforms.select(editor, sel);
56
+ editor.setSavedSelection(sel);
57
+ });
58
+
59
+ return true;
60
+ }
61
+
62
+ return false;
63
+ };
64
+
65
+ /**
66
+ * Will unwrap a node that has as type the one received or one from an array.
67
+ * It identifies the current target element and expands the selection to it, in
68
+ * case the selection was just partial. This allows a "clear and reassign"
69
+ * operation, for example for the Link plugin.
70
+ *
71
+ * @param {string|Object[]} elementType - this can be a string or an array of strings
72
+ * @returns {Object|null} - current node
73
+ */
74
+ export const _unwrapElement = (elementType) => (editor) => {
75
+ const selection = editor.selection || editor.getSavedSelection();
76
+ let [link] = Editor.nodes(editor, {
77
+ at: selection,
78
+ match: (node) => node?.type === elementType,
79
+ });
80
+ const isAtStart =
81
+ selection.anchor.offset === 0 && selection.focus.offset === 0;
82
+
83
+ if (!link && !isAtStart) return false;
84
+
85
+ if (!link) {
86
+ try {
87
+ link = Editor.previous(editor, {
88
+ at: selection.anchor.path,
89
+ });
90
+ } catch (ex) {
91
+ link = [];
92
+ }
93
+ }
94
+
95
+ const [, path] = link;
96
+ const [start, end] = Editor.edges(editor, path);
97
+ const range = { anchor: start, focus: end };
98
+
99
+ const ref = Editor.rangeRef(editor, range);
100
+
101
+ Transforms.select(editor, range);
102
+ Transforms.unwrapNodes(editor, {
103
+ match: (n) =>
104
+ Array.isArray(elementType)
105
+ ? elementType.includes(n.type)
106
+ : n.type === elementType,
107
+ at: range,
108
+ });
109
+
110
+ const current = ref.current;
111
+ ref.unref();
112
+
113
+ return current;
114
+ };
115
+
116
+ export const _isActiveElement = (elementType) => (editor) => {
117
+ const selection = editor.selection || editor.getSavedSelection();
118
+ let found;
119
+ try {
120
+ found = Array.from(
121
+ Editor.nodes(editor, {
122
+ match: (n) => n.type === elementType,
123
+ at: selection,
124
+ }) || [],
125
+ );
126
+ } catch (e) {
127
+ // eslint-disable-next-line
128
+ // console.warn('Error in finding active element', e);
129
+ return false;
130
+ }
131
+ if (found.length) return true;
132
+
133
+ if (selection) {
134
+ const { path } = selection.anchor;
135
+ const isAtStart =
136
+ selection.anchor.offset === 0 && selection.focus.offset === 0;
137
+
138
+ if (isAtStart) {
139
+ try {
140
+ found = Editor.previous(editor, {
141
+ at: path,
142
+ // match: (n) => n.type === MENTION,
143
+ });
144
+ } catch (ex) {
145
+ found = [];
146
+ }
147
+ if (found && found[0] && found[0].type === elementType) {
148
+ return true;
149
+ }
150
+ }
151
+ }
152
+
153
+ return false;
154
+ };
155
+
156
+ /**
157
+ * Will look for a node that has as type the one received or one from an array
158
+ * @param {string|Object[]} elementType - this can be a string or an array of strings
159
+ * @returns {Object|null} - found node
160
+ */
161
+ export const _getActiveElement = (elementType) => (
162
+ editor,
163
+ direction = 'any',
164
+ ) => {
165
+ const selection = editor.selection || editor.getSavedSelection();
166
+ let found = [];
167
+
168
+ try {
169
+ found = Array.from(
170
+ Editor.nodes(editor, {
171
+ match: (n) =>
172
+ Array.isArray(elementType)
173
+ ? elementType.includes(n.type)
174
+ : n.type === elementType,
175
+ at: selection,
176
+ }),
177
+ );
178
+ } catch (e) {
179
+ return null;
180
+ }
181
+
182
+ if (found.length) return found[0];
183
+
184
+ if (!selection) return null;
185
+
186
+ if (direction === 'any' || direction === 'backward') {
187
+ const { path } = selection.anchor;
188
+ const isAtStart =
189
+ selection.anchor.offset === 0 && selection.focus.offset === 0;
190
+
191
+ if (isAtStart) {
192
+ let found;
193
+ try {
194
+ found = Editor.previous(editor, {
195
+ at: path,
196
+ });
197
+ } catch (ex) {
198
+ // eslint-disable-next-line no-console
199
+ console.warn('Unable to find previous node', editor, path);
200
+ return;
201
+ }
202
+ if (found && found[0] && found[0].type === elementType) {
203
+ if (
204
+ (Array.isArray(elementType) && elementType.includes(found[0].type)) ||
205
+ found[0].type === elementType
206
+ ) {
207
+ return found;
208
+ }
209
+ } else {
210
+ return null;
211
+ }
212
+ }
213
+ }
214
+
215
+ if (direction === 'any' || direction === 'forward') {
216
+ const { path } = selection.anchor;
217
+ const isAtStart =
218
+ selection.anchor.offset === 0 && selection.focus.offset === 0;
219
+
220
+ if (isAtStart) {
221
+ let found;
222
+ try {
223
+ found = Editor.next(editor, {
224
+ at: path,
225
+ });
226
+ } catch (e) {
227
+ // eslint-disable-next-line
228
+ console.warn('Unable to find next node', editor, path);
229
+ return;
230
+ }
231
+ if (found && found[0] && found[0].type === elementType) {
232
+ if (
233
+ (Array.isArray(elementType) && elementType.includes(found[0].type)) ||
234
+ found[0].type === elementType
235
+ ) {
236
+ return found;
237
+ }
238
+ } else {
239
+ return null;
240
+ }
241
+ }
242
+ }
243
+
244
+ return null;
245
+ };
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 4H19V6H8V12H18V14H8V21H6V4Z"></path></svg>
package/src/index.js CHANGED
@@ -373,6 +373,7 @@ const applyConfig = (config) => {
373
373
  },
374
374
  ];
375
375
 
376
+ // Install slate
376
377
  config = installSlate(config);
377
378
 
378
379
  // Custom block-style colors
package/src/slate.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import React from 'react';
2
+ import cx from 'classnames';
2
3
  import { List } from 'semantic-ui-react';
3
- import { MarkElementButton, ToolbarButton } from '@plone/volto-slate/editor/ui';
4
+ import {
5
+ MarkElementButton,
6
+ ToolbarButton,
7
+ BlockButton,
8
+ } from '@plone/volto-slate/editor/ui';
4
9
  import installCallout from '@plone/volto-slate/editor/plugins/Callout';
5
10
  import { Icon } from '@plone/volto/components';
6
11
  import { Editor, Transforms, Text } from 'slate';
@@ -14,6 +19,39 @@ import alignJustifyIcon from '@plone/volto/icons/align-justify.svg';
14
19
  import lightIcon from './icons/light.svg';
15
20
  import smallIcon from './icons/small.svg';
16
21
  import clearIcon from './icons/eraser.svg';
22
+ import fontMono from './icons/font-mono.svg';
23
+
24
+ const installSlateToolbarButton = ({
25
+ config,
26
+ key,
27
+ before,
28
+ button,
29
+ element,
30
+ }) => {
31
+ const toolbarButtons = config.settings.slate.toolbarButtons;
32
+ const index = toolbarButtons.indexOf(key);
33
+ const beforeIndex = toolbarButtons.indexOf(before);
34
+ if (index === -1) {
35
+ if (beforeIndex > -1) {
36
+ toolbarButtons.splice(beforeIndex + 1, 0, key);
37
+ } else {
38
+ toolbarButtons.push(key);
39
+ }
40
+ } else if (index > -1 && beforeIndex > -1 && index > beforeIndex + 1) {
41
+ toolbarButtons.splice(index, 1);
42
+ toolbarButtons.splice(beforeIndex + 1, 0, key);
43
+ } else if (index > -1 && index < beforeIndex) {
44
+ toolbarButtons.splice(index, 1);
45
+ toolbarButtons.splice(beforeIndex, 0, key);
46
+ }
47
+ if (button) {
48
+ config.settings.slate.buttons[key] = button;
49
+ }
50
+ if (element) {
51
+ config.settings.slate.elements[key] = element;
52
+ }
53
+ return config;
54
+ };
17
55
 
18
56
  const toggleBlockClassFormat = (editor, format) => {
19
57
  const levels = Array.from(Editor.levels(editor, editor.selection));
@@ -67,20 +105,16 @@ const clearFormatting = (editor) => {
67
105
  Editor.nodes(editor, {
68
106
  mode: 'lowest',
69
107
  match: (n, p) => {
70
- // console.log('node', n, p);
71
108
  return Text.isText(n);
72
109
  },
73
110
  //at: [0], // uncomment if you want everything to be cleared
74
111
  }),
75
112
  );
76
113
 
77
- // console.log('sn', sn);
78
-
79
114
  sn.forEach(([n, at]) => {
80
115
  const toRemove = Object.keys(n).filter((k) => k.startsWith('style-'));
81
116
  if (toRemove.length) {
82
117
  Transforms.unsetNodes(editor, toRemove, { at });
83
- // console.log('unset', n, at, toRemove);
84
118
  }
85
119
  });
86
120
 
@@ -111,9 +145,46 @@ const ClearFormattingButton = ({ icon, ...props }) => {
111
145
 
112
146
  export default function installSlate(config) {
113
147
  if (config.settings.slate) {
148
+ let renderLinkElement;
114
149
  // Callout slate button
115
150
  config = installCallout(config);
116
151
 
152
+ try {
153
+ renderLinkElement = require('@eeacms/volto-anchors/helpers')
154
+ .renderLinkElement;
155
+ } catch {}
156
+
157
+ installSlateToolbarButton({
158
+ config,
159
+ key: 'h3-light',
160
+ before: 'heading-three',
161
+ button: (props) => (
162
+ <BlockButton
163
+ title="Figure title"
164
+ format="h3-light"
165
+ allowedChildren={config.settings.slate.allowedHeadlineElements}
166
+ icon={fontMono}
167
+ {...props}
168
+ />
169
+ ),
170
+ element: renderLinkElement
171
+ ? (opts) => {
172
+ return renderLinkElement('h3')({
173
+ ...opts,
174
+ className: 'subtitle-light',
175
+ });
176
+ }
177
+ : ({ attributes, children }) => (
178
+ <h3
179
+ {...attributes}
180
+ className={cx(attributes.className, 'subtitle-light')}
181
+ >
182
+ {children}
183
+ </h3>
184
+ ),
185
+ });
186
+ config.settings.slate.topLevelTargetElements.push('h3-light');
187
+
117
188
  config.settings.slate.buttons.clearformatting = (props) => (
118
189
  <ClearFormattingButton title="Clear formatting" icon={clearIcon} />
119
190
  );
@@ -1,106 +0,0 @@
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);
@@ -1,479 +0,0 @@
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;