@eeacms/volto-eea-website-theme 1.32.1 → 1.33.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/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ 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.33.0](https://github.com/eea/volto-eea-website-theme/compare/1.32.1...1.33.0) - 2 April 2024
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat(columnsBlock): move tocEntries definition to volto-columns-block [Miu Razvan - [`eb55ff8`](https://github.com/eea/volto-eea-website-theme/commit/eb55ff8753e443c83fd4a8bb3dca8c2b2a78a782)]
12
+
13
+ #### :bug: Bug Fixes
14
+
15
+ - fix(toc): use the correct function to render entries in toc [Miu Razvan - [`869ae7c`](https://github.com/eea/volto-eea-website-theme/commit/869ae7cbe2ec555ebfe563d66bb00e5bc80651c2)]
16
+
17
+ #### :house: Internal changes
18
+
19
+ - chore: Cleanup package.json [alin - [`c5a1c37`](https://github.com/eea/volto-eea-website-theme/commit/c5a1c370eec27a0ebea9df51dde12040580def2f)]
20
+
21
+ #### :hammer_and_wrench: Others
22
+
23
+ - Update index.js to fix jslint issue [ichim-david - [`89b9cef`](https://github.com/eea/volto-eea-website-theme/commit/89b9cefbe35f780bdd07fa6f44234fbfc2fe7bb0)]
24
+ - Update package.json [ichim-david - [`a0ecadf`](https://github.com/eea/volto-eea-website-theme/commit/a0ecadf6efcdeb0fec4ee533d27a6c6787029373)]
25
+ - Revert "fix(blocks): Allow image block urls to be external (#214)" [David Ichim - [`2bbc620`](https://github.com/eea/volto-eea-website-theme/commit/2bbc620d0a375d69ba7982c8e3113628365bb8da)]
7
26
  ### [1.32.1](https://github.com/eea/volto-eea-website-theme/compare/1.32.0...1.32.1) - 26 March 2024
8
27
 
9
28
  #### :rocket: New Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "1.32.1",
3
+ "version": "1.33.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",
@@ -79,4 +79,4 @@
79
79
  "cypress:open": "make cypress-open",
80
80
  "prepare": "husky install"
81
81
  }
82
- }
82
+ }
@@ -0,0 +1,68 @@
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 LayoutSettingsView from './LayoutSettingsView';
8
+
9
+ const mockStore = configureStore();
10
+ let history = createMemoryHistory();
11
+
12
+ describe('LayoutSettingsView Component', () => {
13
+ it('renders without crashing', () => {
14
+ const store = mockStore({
15
+ intl: {
16
+ locale: 'en',
17
+ messages: {},
18
+ },
19
+ });
20
+
21
+ const data = {
22
+ '@layout': 'e28ec238-4cd7-4b72-8025-66da44a6062f',
23
+ '@type': 'layoutSettings',
24
+ block: '87911ec6-4242-4bae-b6a5-9b28151169fa',
25
+ body_class: 'body-class-1',
26
+ layout_size: 'container_view',
27
+ };
28
+
29
+ const { container } = render(
30
+ <Provider store={store}>
31
+ <Router history={history}>
32
+ <LayoutSettingsView data={data} />
33
+ </Router>
34
+ </Provider>,
35
+ );
36
+
37
+ expect(container).toBeTruthy();
38
+ });
39
+ });
40
+
41
+ describe('LayoutSettingsView Component', () => {
42
+ it('renders without crashing with multiple classes', () => {
43
+ const store = mockStore({
44
+ intl: {
45
+ locale: 'en',
46
+ messages: {},
47
+ },
48
+ });
49
+
50
+ const data = {
51
+ '@layout': 'e28ec238-4cd7-4b72-8025-66da44a6062f',
52
+ '@type': 'layoutSettings',
53
+ block: '87911ec6-4242-4bae-b6a5-9b28151169fa',
54
+ body_class: ['body-class-1', 'body-class-2'],
55
+ layout_size: 'container_view',
56
+ };
57
+
58
+ const { container } = render(
59
+ <Provider store={store}>
60
+ <Router history={history}>
61
+ <LayoutSettingsView data={data} />
62
+ </Router>
63
+ </Provider>,
64
+ );
65
+
66
+ expect(container).toBeTruthy();
67
+ });
68
+ });
@@ -32,6 +32,7 @@ export const EditSchema = () => {
32
32
  ['homepage', 'Homepage'],
33
33
  ['homepage-inverse', 'Homepage inverse'],
34
34
  ],
35
+ widget: 'creatable_select',
35
36
  },
36
37
  },
37
38
  };
@@ -0,0 +1,304 @@
1
+ /**
2
+ * CreatableSelectWidget component.
3
+ * @module components/manage/Widgets/SelectWidget
4
+ *
5
+ * A copy of the SelectWidget component. The only difference is that is uses the Creatable component as a base
6
+ */
7
+
8
+ import React, { Component } from 'react';
9
+ import PropTypes from 'prop-types';
10
+ import { connect } from 'react-redux';
11
+ import { compose } from 'redux';
12
+ import { map } from 'lodash';
13
+ import { defineMessages, injectIntl } from 'react-intl';
14
+ import {
15
+ getVocabFromHint,
16
+ getVocabFromField,
17
+ getVocabFromItems,
18
+ } from '@plone/volto/helpers';
19
+ import { FormFieldWrapper } from '@plone/volto/components';
20
+ import { getVocabulary, getVocabularyTokenTitle } from '@plone/volto/actions';
21
+ import { normalizeValue } from '@plone/volto/components/manage/Widgets/SelectUtils';
22
+
23
+ import {
24
+ customSelectStyles,
25
+ DropdownIndicator,
26
+ ClearIndicator,
27
+ Option,
28
+ selectTheme,
29
+ MenuList,
30
+ MultiValueContainer,
31
+ } from '@plone/volto/components/manage/Widgets/SelectStyling';
32
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
33
+
34
+ import loadable from '@loadable/component';
35
+
36
+ export const Creatable = loadable(() => import('react-select/creatable'));
37
+
38
+ const messages = defineMessages({
39
+ default: {
40
+ id: 'Default',
41
+ defaultMessage: 'Default',
42
+ },
43
+ idTitle: {
44
+ id: 'Short Name',
45
+ defaultMessage: 'Short Name',
46
+ },
47
+ idDescription: {
48
+ id: 'Used for programmatic access to the fieldset.',
49
+ defaultMessage: 'Used for programmatic access to the fieldset.',
50
+ },
51
+ title: {
52
+ id: 'Title',
53
+ defaultMessage: 'Title',
54
+ },
55
+ description: {
56
+ id: 'Description',
57
+ defaultMessage: 'Description',
58
+ },
59
+ close: {
60
+ id: 'Close',
61
+ defaultMessage: 'Close',
62
+ },
63
+ choices: {
64
+ id: 'Choices',
65
+ defaultMessage: 'Choices',
66
+ },
67
+ required: {
68
+ id: 'Required',
69
+ defaultMessage: 'Required',
70
+ },
71
+ select: {
72
+ id: 'Select…',
73
+ defaultMessage: 'Select…',
74
+ },
75
+ no_value: {
76
+ id: 'No value',
77
+ defaultMessage: 'No value',
78
+ },
79
+ no_options: {
80
+ id: 'No options',
81
+ defaultMessage: 'No options',
82
+ },
83
+ });
84
+
85
+ /**
86
+ * SelectWidget component class.
87
+ * @function SelectWidget
88
+ * @returns {string} Markup of the component.
89
+ */
90
+ class SelectWidget extends Component {
91
+ /**
92
+ * Property types.
93
+ * @property {Object} propTypes Property types.
94
+ * @static
95
+ */
96
+ static propTypes = {
97
+ id: PropTypes.string.isRequired,
98
+ title: PropTypes.string.isRequired,
99
+ description: PropTypes.string,
100
+ required: PropTypes.bool,
101
+ error: PropTypes.arrayOf(PropTypes.string),
102
+ getVocabulary: PropTypes.func.isRequired,
103
+ getVocabularyTokenTitle: PropTypes.func.isRequired,
104
+ choices: PropTypes.arrayOf(
105
+ PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
106
+ ),
107
+ items: PropTypes.shape({
108
+ vocabulary: PropTypes.object,
109
+ }),
110
+ widgetOptions: PropTypes.shape({
111
+ vocabulary: PropTypes.object,
112
+ }),
113
+ value: PropTypes.oneOfType([
114
+ PropTypes.object,
115
+ PropTypes.string,
116
+ PropTypes.bool,
117
+ PropTypes.func,
118
+ PropTypes.array,
119
+ ]),
120
+ onChange: PropTypes.func.isRequired,
121
+ onBlur: PropTypes.func,
122
+ onClick: PropTypes.func,
123
+ onEdit: PropTypes.func,
124
+ onDelete: PropTypes.func,
125
+ wrapped: PropTypes.bool,
126
+ noValueOption: PropTypes.bool,
127
+ customOptionStyling: PropTypes.any,
128
+ isMulti: PropTypes.bool,
129
+ placeholder: PropTypes.string,
130
+ };
131
+
132
+ /**
133
+ * Default properties
134
+ * @property {Object} defaultProps Default properties.
135
+ * @static
136
+ */
137
+ static defaultProps = {
138
+ description: null,
139
+ required: false,
140
+ items: {
141
+ vocabulary: null,
142
+ },
143
+ widgetOptions: {
144
+ vocabulary: null,
145
+ },
146
+ error: [],
147
+ choices: [],
148
+ value: null,
149
+ onChange: () => {},
150
+ onBlur: () => {},
151
+ onClick: () => {},
152
+ onEdit: null,
153
+ onDelete: null,
154
+ noValueOption: true,
155
+ customOptionStyling: null,
156
+ };
157
+
158
+ /**
159
+ * Component did mount
160
+ * @method componentDidMount
161
+ * @returns {undefined}
162
+ */
163
+ componentDidMount() {
164
+ if (
165
+ (!this.props.choices || this.props.choices?.length === 0) &&
166
+ this.props.vocabBaseUrl
167
+ ) {
168
+ this.props.getVocabulary({
169
+ vocabNameOrURL: this.props.vocabBaseUrl,
170
+ size: -1,
171
+ subrequest: this.props.lang,
172
+ });
173
+ }
174
+ }
175
+
176
+ componentDidUpdate(prevProps) {
177
+ if (
178
+ this.props.vocabBaseUrl !== prevProps.vocabBaseUrl &&
179
+ (!this.props.choices || this.props.choices?.length === 0)
180
+ ) {
181
+ this.props.getVocabulary({
182
+ vocabNameOrURL: this.props.vocabBaseUrl,
183
+ size: -1,
184
+ subrequest: this.props.lang,
185
+ });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Render method.
191
+ * @method render
192
+ * @returns {string} Markup for the component.
193
+ */
194
+ render() {
195
+ const { id, choices, value, intl, onChange } = this.props;
196
+ const normalizedValue = normalizeValue(choices, value, intl);
197
+ // Make sure that both disabled and isDisabled (from the DX layout feat work)
198
+ const disabled = this.props.disabled || this.props.isDisabled;
199
+
200
+ let options = this.props.vocabBaseUrl
201
+ ? this.props.choices
202
+ : [
203
+ ...map(choices, (option) => ({
204
+ value: option[0],
205
+ label:
206
+ // Fix "None" on the serializer, to remove when fixed in p.restapi
207
+ option[1] !== 'None' && option[1] ? option[1] : option[0],
208
+ })),
209
+ // Only set "no-value" option if there's no default in the field
210
+ // TODO: also if this.props.defaultValue?
211
+ ...(this.props.noValueOption &&
212
+ (this.props.default === undefined || this.props.default === null)
213
+ ? [
214
+ {
215
+ label: this.props.intl.formatMessage(messages.no_value),
216
+ value: 'no-value',
217
+ },
218
+ ]
219
+ : []),
220
+ ];
221
+
222
+ return (
223
+ <FormFieldWrapper {...this.props}>
224
+ <Creatable
225
+ isMulti
226
+ id={`field-${id}`}
227
+ key={choices}
228
+ name={id}
229
+ menuShouldScrollIntoView={false}
230
+ isDisabled={disabled}
231
+ isSearchable={true}
232
+ className="react-select-container"
233
+ classNamePrefix="react-select"
234
+ options={options}
235
+ styles={customSelectStyles}
236
+ theme={selectTheme}
237
+ components={{
238
+ ...(options?.length > 25 && {
239
+ MenuList,
240
+ }),
241
+ MultiValueContainer,
242
+ DropdownIndicator,
243
+ ClearIndicator,
244
+ Option: this.props.customOptionStyling || Option,
245
+ }}
246
+ value={normalizedValue}
247
+ placeholder={
248
+ this.props.placeholder ??
249
+ this.props.intl.formatMessage(messages.select)
250
+ }
251
+ onChange={(selectedOption) => {
252
+ return onChange(
253
+ id,
254
+ selectedOption.map((el) => el.value),
255
+ );
256
+ }}
257
+ isClearable
258
+ />
259
+ </FormFieldWrapper>
260
+ );
261
+ }
262
+ }
263
+
264
+ export const SelectWidgetComponent = injectIntl(SelectWidget);
265
+
266
+ export default compose(
267
+ injectLazyLibs(['reactSelect']),
268
+ connect(
269
+ (state, props) => {
270
+ const vocabBaseUrl = !props.choices
271
+ ? getVocabFromHint(props) ||
272
+ getVocabFromField(props) ||
273
+ getVocabFromItems(props)
274
+ : '';
275
+
276
+ const vocabState =
277
+ state.vocabularies?.[vocabBaseUrl]?.subrequests?.[state.intl.locale];
278
+
279
+ // If the schema already has the choices in it, then do not try to get the vocab,
280
+ // even if there is one
281
+ if (props.choices) {
282
+ return {
283
+ choices: props.choices,
284
+ lang: state.intl.locale,
285
+ };
286
+ } else if (vocabState) {
287
+ return {
288
+ vocabBaseUrl,
289
+ choices: vocabState?.items ?? [],
290
+ lang: state.intl.locale,
291
+ };
292
+ // There is a moment that vocabState is not there yet, so we need to pass the
293
+ // vocabBaseUrl to the component.
294
+ } else if (vocabBaseUrl) {
295
+ return {
296
+ vocabBaseUrl,
297
+ lang: state.intl.locale,
298
+ };
299
+ }
300
+ return { lang: state.intl.locale };
301
+ },
302
+ { getVocabulary, getVocabularyTokenTitle },
303
+ ),
304
+ )(SelectWidgetComponent);
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import configureStore from 'redux-mock-store';
3
+ import { Provider } from 'react-intl-redux';
4
+ import { waitFor, render, screen, fireEvent } from '@testing-library/react';
5
+
6
+ import CreatableSelectWidget from './CreatableSelectWidget';
7
+
8
+ const mockStore = configureStore();
9
+
10
+ jest.mock('@plone/volto/helpers/Loadable/Loadable');
11
+ beforeAll(
12
+ async () =>
13
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
14
+ );
15
+
16
+ test('renders a select widget component', async () => {
17
+ const store = mockStore({
18
+ intl: {
19
+ locale: 'en',
20
+ messages: {},
21
+ },
22
+ vocabularies: {
23
+ 'plone.app.vocabularies.Keywords': {
24
+ items: [{ title: 'My item', value: 'myitem' }],
25
+ itemsTotal: 1,
26
+ },
27
+ },
28
+ });
29
+
30
+ const { container } = render(
31
+ <Provider store={store}>
32
+ <CreatableSelectWidget
33
+ id="my-field"
34
+ title="My field"
35
+ fieldSet="default"
36
+ onChange={() => {}}
37
+ onBlur={() => {}}
38
+ onClick={() => {}}
39
+ />
40
+ </Provider>,
41
+ );
42
+
43
+ await waitFor(() => screen.getByText('My field'));
44
+ expect(container).toBeTruthy();
45
+ });
46
+
47
+ test("No 'No value' option when default value is 0", async () => {
48
+ const store = mockStore({
49
+ intl: {
50
+ locale: 'en',
51
+ messages: {},
52
+ },
53
+ });
54
+
55
+ const choices = [
56
+ ['0', 'None'],
57
+ ['1', 'One'],
58
+ ];
59
+
60
+ const value = {
61
+ value: '0',
62
+ label: 'None',
63
+ };
64
+
65
+ const _default = 0;
66
+
67
+ const { container } = render(
68
+ <Provider store={store}>
69
+ <CreatableSelectWidget
70
+ id="my-field"
71
+ title="My field"
72
+ fieldSet="default"
73
+ choices={choices}
74
+ default={_default}
75
+ value={value}
76
+ onChange={() => {}}
77
+ onBlur={() => {}}
78
+ onClick={() => {}}
79
+ />
80
+ </Provider>,
81
+ );
82
+
83
+ await waitFor(() => screen.getByText('None'));
84
+ fireEvent.mouseDown(
85
+ container.querySelector('.react-select__dropdown-indicator'),
86
+ { button: 0 },
87
+ );
88
+ expect(container).toBeTruthy();
89
+ });
@@ -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'] ?? (data?.href || '');
23
+ const href = data?.href?.[0]?.['@id'] || '';
24
24
  const { copyright, copyrightIcon, copyrightPosition } = data;
25
25
  // const [hovering, setHovering] = React.useState(false);
26
26
  const [viewLoaded, setViewLoaded] = React.useState(false);
@@ -66,7 +66,13 @@ export const getVoltoStyles = (props) => {
66
66
  if (styles[key] === true) {
67
67
  output[key] = key;
68
68
  } else {
69
- output[value] = value;
69
+ if (Array.isArray(value)) {
70
+ value.forEach((el, i) => {
71
+ output[el] = el;
72
+ });
73
+ } else {
74
+ output[value] = value;
75
+ }
70
76
  }
71
77
  }
72
78
  return output;
package/src/index.js CHANGED
@@ -6,8 +6,9 @@ import HomePageView from '@eeacms/volto-eea-website-theme/components/theme/Homep
6
6
  import NotFound from '@eeacms/volto-eea-website-theme/components/theme/NotFound/NotFound';
7
7
  import { TokenWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TokenWidget';
8
8
  import { TopicsWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/TopicsWidget';
9
+ import CreatableSelectWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/CreatableSelectWidget';
10
+
9
11
  import { Icon } from '@plone/volto/components';
10
- import { getBlocks } from '@plone/volto/helpers';
11
12
  import { serializeNodesToText } from '@plone/volto-slate/editor/render';
12
13
  import Tag from '@eeacms/volto-eea-design-system/ui/Tag/Tag';
13
14
 
@@ -301,26 +302,6 @@ const applyConfig = (config) => {
301
302
  // Apply columns block customization
302
303
  if (config.blocks.blocksConfig.columnsBlock) {
303
304
  config.blocks.blocksConfig.columnsBlock.available_colors = eea.colors;
304
- config.blocks.blocksConfig.columnsBlock.tocEntries = (
305
- tocData,
306
- block = {},
307
- ) => {
308
- // integration with volto-block-toc
309
- const headlines = tocData.levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
310
- let entries = [];
311
- const sorted_column_blocks = getBlocks(block?.data || {});
312
- sorted_column_blocks.forEach((column_block) => {
313
- const sorted_blocks = getBlocks(column_block[1]);
314
- sorted_blocks.forEach((block) => {
315
- const { value, plaintext } = block[1];
316
- const type = value?.[0]?.type;
317
- if (headlines.includes(type)) {
318
- entries.push([parseInt(type.slice(1)), plaintext, block[0]]);
319
- }
320
- });
321
- });
322
- return entries;
323
- };
324
305
  }
325
306
 
326
307
  // Description block custom CSS
@@ -336,6 +317,7 @@ const applyConfig = (config) => {
336
317
  config.widgets.views.id.topics = TopicsWidget;
337
318
  config.widgets.views.id.subjects = TokenWidget;
338
319
  config.widgets.views.widget.tags = TokenWidget;
320
+ config.widgets.widget.creatable_select = CreatableSelectWidget;
339
321
 
340
322
  // /voltoCustom.css express-middleware
341
323
  // /ok express-middleware - see also: https://github.com/plone/volto/pull/4432
package/src/index.test.js CHANGED
@@ -91,6 +91,7 @@ describe('applyConfig', () => {
91
91
  tags: undefined,
92
92
  },
93
93
  },
94
+ widget: {},
94
95
  },
95
96
  settings: {
96
97
  eea: {
@@ -243,6 +244,7 @@ describe('applyConfig', () => {
243
244
  tags: undefined,
244
245
  },
245
246
  },
247
+ widget: {},
246
248
  },
247
249
  settings: {
248
250
  eea: {},