@eeacms/volto-eea-website-theme 3.5.5 → 3.6.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,7 +4,32 @@ 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
- ### [3.5.5](https://github.com/eea/volto-eea-website-theme/compare/3.5.4...3.5.5) - 15 April 2025
7
+ ### [3.6.1](https://github.com/eea/volto-eea-website-theme/compare/3.6.0...3.6.1) - 7 May 2025
8
+
9
+ #### :nail_care: Enhancements
10
+
11
+ - change(item): added one column for flex-group refs #279420 [David Ichim - [`139f614`](https://github.com/eea/volto-eea-website-theme/commit/139f614658ef431f9b347a851d2ad6c015d5031e)]
12
+ - change(lead-image): add lazy loading for better performance when using LeadImage [David Ichim - [`a388f7f`](https://github.com/eea/volto-eea-website-theme/commit/a388f7fa3d33de5e14f4258e76fcab9cbce57af5)]
13
+
14
+ #### :house: Internal changes
15
+
16
+ - chore(tests): fix unittests [David Ichim - [`db15cde`](https://github.com/eea/volto-eea-website-theme/commit/db15cde41e2528d7afcc7ef2556499ea5d74a3e3)]
17
+
18
+ #### :hammer_and_wrench: Others
19
+
20
+ - fix uuid mock from previous commit [David Ichim - [`30fac90`](https://github.com/eea/volto-eea-website-theme/commit/30fac9067b19ed260649c13ade283132b2e660dd)]
21
+ ### [3.6.0](https://github.com/eea/volto-eea-website-theme/compare/3.5.5...3.6.0) - 30 April 2025
22
+
23
+ #### :bug: Bug Fixes
24
+
25
+ - fix: Create widgets for creators and contributors full name - refs #284007 [Teodor Voicu - [`a3f73ae`](https://github.com/eea/volto-eea-website-theme/commit/a3f73ae02a6a8f92793d7e747e7ba9bbe1f51e05)]
26
+ - fix: Add email tooltip for users in creators and contributors metadata - refs #274362 [Teodor Voicu - [`f00d2bc`](https://github.com/eea/volto-eea-website-theme/commit/f00d2bcb5c0115db40d95f7e4d9618ce7164b71a)]
27
+
28
+ #### :hammer_and_wrench: Others
29
+
30
+ - Release 3.6.0 [Alin Voinea - [`645d434`](https://github.com/eea/volto-eea-website-theme/commit/645d434318c3cf5a0f1765fa0aa539cb2ecedd74)]
31
+ - Update ImageViewWidget [Miu Razvan - [`7b574be`](https://github.com/eea/volto-eea-website-theme/commit/7b574beac1ef64f1ec54c8f42b4c907291d182ea)]
32
+ ### [3.5.5](https://github.com/eea/volto-eea-website-theme/compare/3.5.4...3.5.5) - 25 April 2025
8
33
 
9
34
  #### :bug: Bug Fixes
10
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-theme",
3
- "version": "3.5.5",
3
+ "version": "3.6.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",
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen } from '@testing-library/react';
4
+ import WebReport from './WebReport';
5
+
6
+ // Mock Portal since we are not in real DOM
7
+ jest.mock('react-portal', () => ({
8
+ Portal: ({ children }) => <div data-testid="portal">{children}</div>,
9
+ }));
10
+
11
+ jest.mock('@plone/volto/helpers', () => ({
12
+ BodyClass: ({ className }) => (
13
+ <div data-testid="body-class" className={className} />
14
+ ),
15
+ }));
16
+
17
+ jest.mock('@plone/volto/components', () => ({
18
+ MaybeWrap: ({ condition, as: As, children }) =>
19
+ condition ? <As>{children}</As> : children,
20
+ }));
21
+
22
+ jest.mock(
23
+ '@eeacms/volto-eea-website-theme/components/theme/Banner/View',
24
+ () => (props) => (
25
+ <div data-testid="banner-view">
26
+ {props.data.aboveTitle}
27
+ {props.data.belowTitle}
28
+ </div>
29
+ ),
30
+ );
31
+
32
+ jest.mock('@eeacms/volto-eea-design-system/ui/Banner/Banner', () => {
33
+ const Subtitle = ({ children }) => (
34
+ <div data-testid="banner-subtitle">{children}</div>
35
+ );
36
+ return {
37
+ Subtitle,
38
+ };
39
+ });
40
+
41
+ describe('WebReport', () => {
42
+ it('renders with content type and subtitle', () => {
43
+ const props = {
44
+ isEditMode: false,
45
+ data: {
46
+ hero_header: true,
47
+ content_type: 'Report',
48
+ subtitle: 'This is a subtitle',
49
+ },
50
+ properties: {
51
+ type_title: 'Fallback Title',
52
+ },
53
+ };
54
+
55
+ render(<WebReport {...props} />);
56
+
57
+ // Check portal wrapping
58
+ expect(screen.getByTestId('portal')).toBeInTheDocument();
59
+
60
+ // Check BodyClass applied
61
+ expect(screen.getByTestId('body-class')).toHaveClass(
62
+ 'homepage-inverse',
63
+ 'homepage-header',
64
+ 'light-header',
65
+ 'hero-header',
66
+ );
67
+
68
+ // Check BannerView rendered
69
+ expect(screen.getByTestId('banner-view')).toBeInTheDocument();
70
+
71
+ // Content Type shown
72
+ expect(screen.getByText('Report')).toBeInTheDocument();
73
+
74
+ // Subtitle shown
75
+ expect(screen.getByText('This is a subtitle')).toBeInTheDocument();
76
+ });
77
+
78
+ it('renders fallback type_title when content_type missing', () => {
79
+ const props = {
80
+ isEditMode: false,
81
+ data: {
82
+ hero_header: true,
83
+ subtitle: 'Another subtitle',
84
+ },
85
+ properties: {
86
+ type_title: 'Fallback Title',
87
+ },
88
+ };
89
+
90
+ render(<WebReport {...props} />);
91
+
92
+ // Fallback title used
93
+ expect(screen.getByText('Fallback Title')).toBeInTheDocument();
94
+ });
95
+
96
+ it('hides content type if hideContentType is true', () => {
97
+ const props = {
98
+ isEditMode: false,
99
+ data: {
100
+ hero_header: false,
101
+ hideContentType: true,
102
+ subtitle: 'Hidden subtitle',
103
+ },
104
+ properties: {
105
+ type_title: 'Hidden Title',
106
+ },
107
+ };
108
+
109
+ render(<WebReport {...props} />);
110
+
111
+ // Should NOT find content type div
112
+ expect(screen.queryByText('Hidden Title')).not.toBeInTheDocument();
113
+ });
114
+
115
+ it('renders directly without portal in edit mode', () => {
116
+ const props = {
117
+ isEditMode: true,
118
+ data: {
119
+ subtitle: 'Edit mode subtitle',
120
+ },
121
+ properties: {
122
+ type_title: 'Edit Title',
123
+ },
124
+ };
125
+
126
+ render(<WebReport {...props} />);
127
+
128
+ // Should not wrap with Portal when in edit mode
129
+ expect(screen.queryByTestId('portal')).not.toBeInTheDocument();
130
+
131
+ // Banner still rendered
132
+ expect(screen.getByTestId('banner-view')).toBeInTheDocument();
133
+ });
134
+ });
@@ -0,0 +1,23 @@
1
+ import cx from 'classnames';
2
+
3
+ const ContributorsViewWidget = ({ value, content, children, className }) => {
4
+ const resolvedValue = content?.contributors_fullname || value || [];
5
+ return resolvedValue ? (
6
+ <span className={cx(className, 'array', 'widget')}>
7
+ {resolvedValue.map((item, key) => (
8
+ <>
9
+ {key ? ', ' : ''}
10
+ <span key={item.token || item.title || item}>
11
+ {children
12
+ ? children(item.title || item.token || item)
13
+ : item.title || item.token || item}
14
+ </span>
15
+ </>
16
+ ))}
17
+ </span>
18
+ ) : (
19
+ ''
20
+ );
21
+ };
22
+
23
+ export default ContributorsViewWidget;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import renderer from 'react-test-renderer';
3
+ import ContributorsViewWidget from './ContributorsViewWidget';
4
+
5
+ describe('ContributorsViewWidget', () => {
6
+ it('renders an empty array view widget component', () => {
7
+ const component = renderer.create(<ContributorsViewWidget />);
8
+ const json = component.toJSON();
9
+ expect(json).toMatchSnapshot();
10
+ });
11
+
12
+ it('renders a simple array view widget component', () => {
13
+ const component = renderer.create(
14
+ <ContributorsViewWidget className="metadata" value={['foo', 'bar']} />,
15
+ );
16
+ const json = component.toJSON();
17
+ expect(json).toMatchSnapshot();
18
+ });
19
+
20
+ it('renders a vocabulary array view widget component', () => {
21
+ const component = renderer.create(
22
+ <ContributorsViewWidget
23
+ className="metadata"
24
+ value={[{ title: 'Foo' }, { title: 'Bar' }]}
25
+ />,
26
+ );
27
+ const json = component.toJSON();
28
+ expect(json).toMatchSnapshot();
29
+ });
30
+
31
+ it('renders a full vocabulary array view widget component', () => {
32
+ const component = renderer.create(
33
+ <ContributorsViewWidget
34
+ className="metadata"
35
+ value={[
36
+ { title: 'Foo', token: 'foo' },
37
+ { title: 'Bar', token: 'bar' },
38
+ ]}
39
+ />,
40
+ );
41
+ const json = component.toJSON();
42
+ expect(json).toMatchSnapshot();
43
+ });
44
+
45
+ it('renders a full vocabulary array view widget component with children', () => {
46
+ const component = renderer.create(
47
+ <ContributorsViewWidget
48
+ className="metadata"
49
+ value={[
50
+ { title: 'Foo', token: 'foo' },
51
+ { title: 'Bar', token: 'bar' },
52
+ ]}
53
+ >
54
+ {(child) => <strong>{child}</strong>}
55
+ </ContributorsViewWidget>,
56
+ );
57
+ const json = component.toJSON();
58
+ expect(json).toMatchSnapshot();
59
+ });
60
+ });
@@ -0,0 +1,23 @@
1
+ import cx from 'classnames';
2
+
3
+ const CreatorsViewWidget = ({ value, content, children, className }) => {
4
+ const resolvedValue = content?.creators_fullname || value || [];
5
+ return resolvedValue ? (
6
+ <span className={cx(className, 'array', 'widget')}>
7
+ {resolvedValue.map((item, key) => (
8
+ <>
9
+ {key ? ', ' : ''}
10
+ <span key={item.token || item.title || item}>
11
+ {children
12
+ ? children(item.title || item.token || item)
13
+ : item.title || item.token || item}
14
+ </span>
15
+ </>
16
+ ))}
17
+ </span>
18
+ ) : (
19
+ ''
20
+ );
21
+ };
22
+
23
+ export default CreatorsViewWidget;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import renderer from 'react-test-renderer';
3
+ import CreatorsViewWidget from './CreatorsViewWidget';
4
+
5
+ describe('CreatorsViewWidget', () => {
6
+ it('renders an empty array view widget component', () => {
7
+ const component = renderer.create(<CreatorsViewWidget />);
8
+ const json = component.toJSON();
9
+ expect(json).toMatchSnapshot();
10
+ });
11
+
12
+ it('renders a simple array view widget component', () => {
13
+ const component = renderer.create(
14
+ <CreatorsViewWidget className="metadata" value={['foo', 'bar']} />,
15
+ );
16
+ const json = component.toJSON();
17
+ expect(json).toMatchSnapshot();
18
+ });
19
+
20
+ it('renders a vocabulary array view widget component', () => {
21
+ const component = renderer.create(
22
+ <CreatorsViewWidget
23
+ className="metadata"
24
+ value={[{ title: 'Foo' }, { title: 'Bar' }]}
25
+ />,
26
+ );
27
+ const json = component.toJSON();
28
+ expect(json).toMatchSnapshot();
29
+ });
30
+
31
+ it('renders a full vocabulary array view widget component', () => {
32
+ const component = renderer.create(
33
+ <CreatorsViewWidget
34
+ className="metadata"
35
+ value={[
36
+ { title: 'Foo', token: 'foo' },
37
+ { title: 'Bar', token: 'bar' },
38
+ ]}
39
+ />,
40
+ );
41
+ const json = component.toJSON();
42
+ expect(json).toMatchSnapshot();
43
+ });
44
+
45
+ it('renders a full vocabulary array view widget component with children', () => {
46
+ const component = renderer.create(
47
+ <CreatorsViewWidget
48
+ className="metadata"
49
+ value={[
50
+ { title: 'Foo', token: 'foo' },
51
+ { title: 'Bar', token: 'bar' },
52
+ ]}
53
+ >
54
+ {(child) => <strong>{child}</strong>}
55
+ </CreatorsViewWidget>,
56
+ );
57
+ const json = component.toJSON();
58
+ expect(json).toMatchSnapshot();
59
+ });
60
+ });
@@ -1,3 +1,3 @@
1
1
  export default function ImageViewWidget({ value }) {
2
- return <img src={value.download} alt={value.filename} />;
2
+ return <img src={value?.download} alt={value?.filename} />;
3
3
  }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * UserSelectWidget component.
3
+ * @module components/manage/Widgets/UserSelectWidget
4
+ */
5
+
6
+ import React, { Component } from 'react';
7
+ import { defineMessages, injectIntl } from 'react-intl';
8
+ import PropTypes from 'prop-types';
9
+ import { compose } from 'redux';
10
+ import { connect } from 'react-redux';
11
+ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
12
+ import { Popup } from 'semantic-ui-react';
13
+ import {
14
+ normalizeValue,
15
+ convertValueToVocabQuery,
16
+ } from '@plone/volto/components/manage/Widgets/SelectUtils';
17
+ import checkSVG from '@plone/volto/icons/check.svg';
18
+ import checkBlankSVG from '@plone/volto/icons/check-blank.svg';
19
+ import { Icon } from '@plone/volto/components';
20
+
21
+ import {
22
+ getVocabFromHint,
23
+ getVocabFromField,
24
+ getVocabFromItems,
25
+ } from '@plone/volto/helpers';
26
+ import { getVocabulary, getVocabularyTokenTitle } from '@plone/volto/actions';
27
+
28
+ import {
29
+ ClearIndicator,
30
+ DropdownIndicator,
31
+ MultiValueContainer,
32
+ selectTheme,
33
+ customSelectStyles,
34
+ MenuList,
35
+ } from '@plone/volto/components/manage/Widgets/SelectStyling';
36
+
37
+ import { FormFieldWrapper } from '@plone/volto/components';
38
+
39
+ const messages = defineMessages({
40
+ select: {
41
+ id: 'Select…',
42
+ defaultMessage: 'Select…',
43
+ },
44
+ no_options: {
45
+ id: 'No options',
46
+ defaultMessage: 'No options',
47
+ },
48
+ type_text: {
49
+ id: 'Type text...',
50
+ defaultMessage: 'Type text...',
51
+ },
52
+ });
53
+
54
+ export const normalizeSingleSelectOption = (value, intl) => {
55
+ if (!value) return value;
56
+
57
+ if (Array.isArray(value)) {
58
+ // Assuming [token, title] pair.
59
+ if (value.length === 2)
60
+ return { value: value[0], label: value[1] || value[0], email: '' };
61
+
62
+ throw new Error(`Unknown value type of select widget: ${value}`);
63
+ }
64
+
65
+ const token = value.token ?? value.value ?? value.UID ?? 'no-value';
66
+ const label =
67
+ (value.title && value.title !== 'None' ? value.title : undefined) ??
68
+ value.label ??
69
+ value.token ??
70
+ intl.formatMessage(messages.no_value);
71
+ return {
72
+ value: token,
73
+ label,
74
+ email: value.email ? value.email : label || token,
75
+ };
76
+ };
77
+
78
+ export const normalizeChoices = (items, intl) =>
79
+ items.map((item) => normalizeSingleSelectOption(item, intl));
80
+
81
+ /**
82
+ * Custom Option component with a tooltip
83
+ */
84
+ const CustomOption = injectLazyLibs('reactSelect')((props) => {
85
+ const { Option } = props.reactSelect.components;
86
+ const color = props.isFocused && !props.isSelected ? '#b8c6c8' : '#007bc1';
87
+ const svgIcon =
88
+ props.isFocused || props.isSelected ? checkSVG : checkBlankSVG;
89
+
90
+ const { data, innerRef, innerProps } = props;
91
+ const { label, email } = data;
92
+
93
+ return (
94
+ <Option {...props}>
95
+ <div ref={innerRef} {...innerProps}>
96
+ <Popup
97
+ content={email}
98
+ position="top center"
99
+ trigger={
100
+ <div
101
+ style={{
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ justifyContent: 'space-between',
105
+ gap: '0.5rem',
106
+ }}
107
+ >
108
+ <span>{label}</span>
109
+ <Icon name={svgIcon} size="20px" color={color} />
110
+ </div>
111
+ }
112
+ />
113
+ </div>
114
+ </Option>
115
+ );
116
+ });
117
+
118
+ /**
119
+ * UserSelectWidget component class.
120
+ * @class UserSelectWidget
121
+ * @extends Component
122
+ */
123
+ class UserSelectWidget extends Component {
124
+ static propTypes = {
125
+ id: PropTypes.string.isRequired,
126
+ title: PropTypes.string.isRequired,
127
+ description: PropTypes.string,
128
+ required: PropTypes.bool,
129
+ error: PropTypes.arrayOf(PropTypes.string),
130
+ getVocabulary: PropTypes.func.isRequired,
131
+ choices: PropTypes.arrayOf(
132
+ PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
133
+ ),
134
+ items: PropTypes.shape({
135
+ vocabulary: PropTypes.object,
136
+ }),
137
+ widgetOptions: PropTypes.shape({
138
+ vocabulary: PropTypes.object,
139
+ }),
140
+ value: PropTypes.arrayOf(
141
+ PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
142
+ ),
143
+ onChange: PropTypes.func.isRequired,
144
+ wrapped: PropTypes.bool,
145
+ isDisabled: PropTypes.bool,
146
+ placeholder: PropTypes.string,
147
+ };
148
+
149
+ static defaultProps = {
150
+ description: null,
151
+ required: false,
152
+ items: {
153
+ vocabulary: null,
154
+ },
155
+ widgetOptions: {
156
+ vocabulary: null,
157
+ },
158
+ error: [],
159
+ choices: [],
160
+ value: null,
161
+ };
162
+
163
+ constructor(props) {
164
+ super(props);
165
+ this.handleChange = this.handleChange.bind(this);
166
+ this.state = {
167
+ searchLength: 0,
168
+ termsPairsCache: [],
169
+ };
170
+ }
171
+
172
+ componentDidMount() {
173
+ const { id, lang, value, choices } = this.props;
174
+ if (value && value?.length > 0) {
175
+ const tokensQuery = convertValueToVocabQuery(
176
+ normalizeValue(choices, value, this.props.intl),
177
+ );
178
+
179
+ this.props.getVocabularyTokenTitle({
180
+ vocabNameOrURL: this.props.vocabBaseUrl,
181
+ subrequest: `widget-${id}-${lang}`,
182
+ ...tokensQuery,
183
+ });
184
+ }
185
+ }
186
+
187
+ componentDidUpdate(prevProps, prevState) {
188
+ const { value, choices } = this.props;
189
+ if (
190
+ this.state.termsPairsCache.length === 0 &&
191
+ value?.length > 0 &&
192
+ choices?.length > 0 &&
193
+ (value !== prevProps.value || choices !== prevProps.choices)
194
+ ) {
195
+ this.setState((state) => ({
196
+ termsPairsCache: [...state.termsPairsCache, ...choices],
197
+ }));
198
+ }
199
+ }
200
+
201
+ componentWillUnmount() {
202
+ if (this.timeoutRef.current) {
203
+ clearTimeout(this.timeoutRef.current);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Handle the field change, store it in the local state and back to simple
209
+ * array of tokens for correct serialization
210
+ * @method handleChange
211
+ * @param {array} selectedOption The selected options (already aggregated).
212
+ * @returns {undefined}
213
+ */
214
+ handleChange(selectedOption) {
215
+ this.props.onChange(
216
+ this.props.id,
217
+ selectedOption ? selectedOption.map((item) => item.value) : null,
218
+ );
219
+ this.setState((state) => ({
220
+ termsPairsCache: [...state.termsPairsCache, ...selectedOption],
221
+ }));
222
+ }
223
+
224
+ timeoutRef = React.createRef();
225
+ // How many characters to hold off searching from. Search tarts at this plus one.
226
+ SEARCH_HOLDOFF = 2;
227
+
228
+ loadOptions = (query) => {
229
+ // Implement a debounce of 400ms and a min search of 3 chars
230
+ if (query.length > this.SEARCH_HOLDOFF) {
231
+ if (this.timeoutRef.current) clearTimeout(this.timeoutRef.current);
232
+ return new Promise((resolve) => {
233
+ this.timeoutRef.current = setTimeout(async () => {
234
+ const res = await this.fetchAvailableChoices(query);
235
+ resolve(res);
236
+ }, 400);
237
+ });
238
+ } else {
239
+ return Promise.resolve([]);
240
+ }
241
+ };
242
+
243
+ fetchAvailableChoices = async (query) => {
244
+ const resp = await this.props.getVocabulary({
245
+ vocabNameOrURL: this.props.vocabBaseUrl,
246
+ query,
247
+ size: -1,
248
+ subrequest: this.props.lang,
249
+ });
250
+ return normalizeChoices(resp.items || [], this.props.intl);
251
+ };
252
+
253
+ render() {
254
+ const selectedOption = normalizeValue(
255
+ this.state.termsPairsCache,
256
+ this.props.value,
257
+ this.props.intl,
258
+ );
259
+ const SelectAsync = this.props.reactSelectAsync.default;
260
+
261
+ return (
262
+ <FormFieldWrapper {...this.props}>
263
+ <SelectAsync
264
+ id={`field-${this.props.id}`}
265
+ key={this.props.id}
266
+ isDisabled={this.props.disabled || this.props.isDisabled}
267
+ className="react-select-container"
268
+ classNamePrefix="react-select"
269
+ cacheOptions
270
+ defaultOptions={[]}
271
+ loadOptions={this.loadOptions}
272
+ onInputChange={(search) =>
273
+ this.setState({ searchLength: search.length })
274
+ }
275
+ noOptionsMessage={() =>
276
+ this.props.intl.formatMessage(
277
+ this.state.searchLength > this.SEARCH_HOLDOFF
278
+ ? messages.no_options
279
+ : messages.type_text,
280
+ )
281
+ }
282
+ styles={customSelectStyles}
283
+ theme={selectTheme}
284
+ components={{
285
+ ...(this.props.choices?.length > 25 && { MenuList }),
286
+ MultiValueContainer,
287
+ ClearIndicator,
288
+ DropdownIndicator,
289
+ Option: CustomOption,
290
+ }}
291
+ value={selectedOption || []}
292
+ placeholder={
293
+ this.props.placeholder ??
294
+ this.props.intl.formatMessage(messages.select)
295
+ }
296
+ onChange={this.handleChange}
297
+ isMulti
298
+ />
299
+ </FormFieldWrapper>
300
+ );
301
+ }
302
+ }
303
+
304
+ export default compose(
305
+ injectIntl,
306
+ injectLazyLibs(['reactSelectAsync']),
307
+ connect(
308
+ (state, props) => {
309
+ const vocabBaseUrl =
310
+ getVocabFromHint(props) ||
311
+ getVocabFromField(props) ||
312
+ getVocabFromItems(props);
313
+
314
+ const vocabState =
315
+ state.vocabularies?.[vocabBaseUrl]?.subrequests?.[
316
+ `widget-${props.id}-${state.intl.locale}`
317
+ ]?.items;
318
+
319
+ return props.items?.choices
320
+ ? { choices: props.items.choices, lang: state.intl.locale }
321
+ : vocabState
322
+ ? {
323
+ choices: vocabState,
324
+ vocabBaseUrl,
325
+ lang: state.intl.locale,
326
+ }
327
+ : { vocabBaseUrl, lang: state.intl.locale };
328
+ },
329
+ { getVocabulary, getVocabularyTokenTitle },
330
+ ),
331
+ )(UserSelectWidget);
@@ -0,0 +1,255 @@
1
+ import React from 'react';
2
+ import configureStore from 'redux-mock-store';
3
+ import { Provider } from 'react-intl-redux';
4
+ import { waitFor, render, screen } from '@testing-library/react';
5
+
6
+ import UserSelectWidget, {
7
+ normalizeChoices,
8
+ normalizeSingleSelectOption,
9
+ } from './UserSelectWidget';
10
+
11
+ const mockStore = configureStore();
12
+
13
+ jest.mock('@plone/volto/helpers/Loadable/Loadable');
14
+ beforeAll(
15
+ async () =>
16
+ await require('@plone/volto/helpers/Loadable/Loadable').__setLoadables(),
17
+ );
18
+
19
+ test('renders a select widget component', async () => {
20
+ const store = mockStore({
21
+ intl: {
22
+ locale: 'en',
23
+ messages: {},
24
+ },
25
+ vocabularies: {
26
+ 'plone.app.vocabularies.Keywords': {
27
+ items: [
28
+ { email: 'myemail@provider.com', title: 'My item', value: 'myitem' },
29
+ ],
30
+ itemsTotal: 1,
31
+ },
32
+ },
33
+ });
34
+
35
+ const props = {
36
+ getVocabulary: () => {
37
+ return Promise.resolve({
38
+ items: [
39
+ { email: 'foo@provider.com', token: 'foo', title: 'Foo' },
40
+ { email: 'bar@provider.com', token: 'bar', title: 'Bar' },
41
+ { email: 'foobar@provider.com', token: 'fooBar', title: 'FooBar' },
42
+ ],
43
+ });
44
+ },
45
+ widgetOptions: {
46
+ vocabulary: { '@id': 'plone.app.vocabularies.Keywords' },
47
+ },
48
+ };
49
+
50
+ const { container } = render(
51
+ <Provider store={store}>
52
+ <UserSelectWidget
53
+ {...props}
54
+ id="my-field"
55
+ title="My field"
56
+ fieldSet="default"
57
+ onChange={() => {}}
58
+ onBlur={() => {}}
59
+ onClick={() => {}}
60
+ />
61
+ </Provider>,
62
+ );
63
+
64
+ await waitFor(() => screen.getByText('My field'));
65
+ expect(container).toMatchSnapshot();
66
+ });
67
+
68
+ // Test normalization of choices
69
+ test('normalizes vocabulary API response correctly', () => {
70
+ const mockData = [
71
+ { email: 'charlie@example.com', token: 'charlie', title: 'Charlie' },
72
+ { email: 'dana@example.com', token: 'dana', title: 'Dana' },
73
+ ];
74
+
75
+ const result = normalizeChoices(mockData, {
76
+ formatMessage: (msg) => msg.defaultMessage,
77
+ });
78
+
79
+ expect(result).toEqual([
80
+ { value: 'charlie', label: 'Charlie', email: 'charlie@example.com' },
81
+ { value: 'dana', label: 'Dana', email: 'dana@example.com' },
82
+ ]);
83
+ });
84
+
85
+ // Test missing email default handling
86
+ test('defaults missing email to label or token', () => {
87
+ const mockData = [{ token: 'no-email', title: 'No Email User' }];
88
+
89
+ const result = normalizeChoices(mockData, {
90
+ formatMessage: (msg) => msg.defaultMessage,
91
+ });
92
+
93
+ expect(result).toEqual([
94
+ { value: 'no-email', label: 'No Email User', email: 'No Email User' },
95
+ ]);
96
+ });
97
+
98
+ // Test search logic (filters results based on query)
99
+ test('filters choices based on search query', () => {
100
+ const mockData = [
101
+ { email: 'george@example.com', token: 'george', title: 'George' },
102
+ { email: 'hannah@example.com', token: 'hannah', title: 'Hannah' },
103
+ ];
104
+
105
+ const result = normalizeChoices(mockData, {
106
+ formatMessage: (msg) => msg.defaultMessage,
107
+ });
108
+
109
+ const filteredResults = result.filter((item) =>
110
+ item.label.toLowerCase().includes('geo'),
111
+ );
112
+
113
+ expect(filteredResults).toEqual([
114
+ { value: 'george', label: 'George', email: 'george@example.com' },
115
+ ]);
116
+ });
117
+
118
+ test('normalizes a valid object with email', () => {
119
+ const result = normalizeSingleSelectOption(
120
+ { token: 'user1', title: 'User One', email: 'user1@example.com' },
121
+ { formatMessage: (msg) => msg.defaultMessage },
122
+ );
123
+
124
+ expect(result).toEqual({
125
+ value: 'user1',
126
+ label: 'User One',
127
+ email: 'user1@example.com',
128
+ });
129
+ });
130
+
131
+ test('normalizes an object with missing email using label fallback', () => {
132
+ const result = normalizeSingleSelectOption(
133
+ { token: 'user2', title: 'User Two' },
134
+ { formatMessage: (msg) => msg.defaultMessage },
135
+ );
136
+
137
+ expect(result).toEqual({
138
+ value: 'user2',
139
+ label: 'User Two',
140
+ email: 'User Two',
141
+ });
142
+ });
143
+
144
+ test('normalizes an array [token, title]', () => {
145
+ const result = normalizeSingleSelectOption(['user3', 'User Three'], {
146
+ formatMessage: (msg) => msg.defaultMessage,
147
+ });
148
+
149
+ expect(result).toEqual({
150
+ value: 'user3',
151
+ label: 'User Three',
152
+ email: '',
153
+ });
154
+ });
155
+
156
+ test('throws an error for unexpected array format', () => {
157
+ expect(() => {
158
+ normalizeSingleSelectOption(['wrongFormat'], {
159
+ formatMessage: (msg) => msg.defaultMessage,
160
+ });
161
+ }).toThrow('Unknown value type of select widget: wrongFormat');
162
+ });
163
+
164
+ test('normalizes an object with only token and falls back correctly', () => {
165
+ const result = normalizeSingleSelectOption(
166
+ { token: 'user4' },
167
+ { formatMessage: (msg) => msg.defaultMessage },
168
+ );
169
+
170
+ expect(result).toEqual({
171
+ value: 'user4',
172
+ label: 'user4',
173
+ email: 'user4',
174
+ });
175
+ });
176
+
177
+ test('returns input when value is null or undefined', () => {
178
+ expect(normalizeSingleSelectOption(null, {})).toBe(null);
179
+ expect(normalizeSingleSelectOption(undefined, {})).toBe(undefined);
180
+ });
181
+
182
+ // Test normalizeSingleSelectOption with an object missing title
183
+ test('normalizes an object with missing title and fallback to token', () => {
184
+ const result = normalizeSingleSelectOption(
185
+ { token: 'user5', email: 'user5@example.com' },
186
+ { formatMessage: (msg) => msg.defaultMessage },
187
+ );
188
+
189
+ expect(result).toEqual({
190
+ value: 'user5',
191
+ label: 'user5',
192
+ email: 'user5@example.com',
193
+ });
194
+ });
195
+
196
+ // Test normalizeChoices with an empty array
197
+ test('handles empty array correctly in normalizeChoices', () => {
198
+ const result = normalizeChoices([], {
199
+ formatMessage: (msg) => msg.defaultMessage,
200
+ });
201
+
202
+ expect(result).toEqual([]);
203
+ });
204
+
205
+ // Test normalizeSingleSelectOption when title is "None"
206
+ test('defaults label to token when title is "None"', () => {
207
+ const result = normalizeSingleSelectOption(
208
+ { token: 'user6', title: 'None', email: 'user6@example.com' },
209
+ { formatMessage: (msg) => msg.defaultMessage },
210
+ );
211
+
212
+ expect(result).toEqual({
213
+ value: 'user6',
214
+ label: 'user6',
215
+ email: 'user6@example.com',
216
+ });
217
+ });
218
+
219
+ // Test normalizeSingleSelectOption with both token and value fields
220
+ test('handles object with both token and value fields', () => {
221
+ const result = normalizeSingleSelectOption(
222
+ { token: 'user7', value: 'actualValue', title: 'User Seven' },
223
+ { formatMessage: (msg) => msg.defaultMessage },
224
+ );
225
+
226
+ expect(result).toEqual({
227
+ value: 'user7',
228
+ label: 'User Seven',
229
+ email: 'User Seven',
230
+ });
231
+ });
232
+
233
+ // Test normalizeChoices with nested objects
234
+ test('ignores extra nested data in normalizeChoices', () => {
235
+ const mockData = [
236
+ {
237
+ email: 'nested@example.com',
238
+ token: 'nestedUser',
239
+ title: 'Nested User',
240
+ extraField: { something: 'should be ignored' },
241
+ },
242
+ ];
243
+
244
+ const result = normalizeChoices(mockData, {
245
+ formatMessage: (msg) => msg.defaultMessage,
246
+ });
247
+
248
+ expect(result).toEqual([
249
+ {
250
+ value: 'nestedUser',
251
+ label: 'Nested User',
252
+ email: 'nested@example.com',
253
+ },
254
+ ]);
255
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Vocabularies actions.
3
+ * @module actions/vocabularies/vocabularies
4
+ */
5
+
6
+ import {
7
+ GET_VOCABULARY,
8
+ GET_VOCABULARY_TOKEN_TITLE,
9
+ } from '@plone/volto/constants/ActionTypes';
10
+ import { getVocabName } from '@plone/volto/helpers/Vocabularies/Vocabularies';
11
+ import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
12
+ import qs from 'query-string';
13
+
14
+ /**
15
+ * Get vocabulary given a URL (coming from a Schema) or from a vocabulary name.
16
+ * @function getVocabulary
17
+ * @param {string} vocabNameOrURL Full API URL of vocabulary or vocabulary name
18
+ * @param {string} query Only include results containing this string.
19
+ * @param {number} start Start of result batch.
20
+ * @param {number} b_size The size of the batch.
21
+ * @param {string} subrequest Name of the subrequest.
22
+ * @returns {Object} Get vocabulary action.
23
+ */
24
+ export function getVocabulary({
25
+ vocabNameOrURL,
26
+ query = null,
27
+ start = 0,
28
+ size,
29
+ subrequest,
30
+ }) {
31
+ const vocabPath = vocabNameOrURL.includes('/')
32
+ ? flattenToAppURL(vocabNameOrURL)
33
+ : `/@vocabularies/${vocabNameOrURL}`;
34
+
35
+ let queryString = `b_start=${start}${size ? '&b_size=' + size : ''}`;
36
+
37
+ if (query) {
38
+ queryString = `${queryString}&title=${query}`;
39
+ }
40
+ return {
41
+ type: GET_VOCABULARY,
42
+ vocabulary: vocabNameOrURL,
43
+ start,
44
+ request: {
45
+ op: 'get',
46
+ path: `${vocabPath}?${queryString}`,
47
+ },
48
+ subrequest,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Get the title value given a token from vocabulary given a vocabulary URL
54
+ * (coming from a Schema) or from a vocabulary name.
55
+ * @function getVocabularyTokenTitle
56
+ * @param {string} vocabNameOrURL Full API URL of vocabulary or vocabulary name
57
+ * @param {string} token Only include results containing this string.
58
+ * @returns {Object} Get vocabulary action.
59
+ */
60
+ export function getVocabularyTokenTitle({
61
+ vocabNameOrURL,
62
+ token = null,
63
+ tokens = null,
64
+ subrequest,
65
+ }) {
66
+ // In case we have a URL, we have to get the vocabulary name
67
+ const vocabulary = getVocabName(vocabNameOrURL);
68
+ const queryString = {
69
+ ...(token && { token }),
70
+ ...(tokens && { tokens }),
71
+ };
72
+
73
+ return {
74
+ type: GET_VOCABULARY_TOKEN_TITLE,
75
+ vocabulary: vocabNameOrURL,
76
+ token,
77
+ tokens,
78
+ subrequest,
79
+ request: {
80
+ op: 'get',
81
+ path: `/@vocabularies/${vocabulary}?b_size=-1&${qs.stringify(
82
+ queryString,
83
+ {
84
+ encode: false,
85
+ },
86
+ )}`,
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,32 @@
1
+ diff --git a/src/customizations/volto/actions/vocabularies/vocabularies.js b/src/customizations/volto/actions/vocabularies/vocabularies.js
2
+ index 811aee2..ec60589 100644
3
+ --- a/src/customizations/volto/actions/vocabularies/vocabularies.js
4
+ +++ b/src/customizations/volto/actions/vocabularies/vocabularies.js
5
+ @@ -8,6 +8,7 @@ import {
6
+ GET_VOCABULARY_TOKEN_TITLE,
7
+ } from '@plone/volto/constants/ActionTypes';
8
+ import { getVocabName } from '@plone/volto/helpers/Vocabularies/Vocabularies';
9
+ +import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
10
+ import qs from 'query-string';
11
+
12
+ /**
13
+ @@ -27,7 +28,9 @@ export function getVocabulary({
14
+ size,
15
+ subrequest,
16
+ }) {
17
+ - const vocabulary = getVocabName(vocabNameOrURL);
18
+ + const vocabPath = vocabNameOrURL.includes('/')
19
+ + ? flattenToAppURL(vocabNameOrURL)
20
+ + : `/@vocabularies/${vocabNameOrURL}`;
21
+
22
+ let queryString = `b_start=${start}${size ? '&b_size=' + size : ''}`;
23
+
24
+ @@ -40,7 +43,7 @@ export function getVocabulary({
25
+ start,
26
+ request: {
27
+ op: 'get',
28
+ - path: `/@vocabularies/${vocabulary}?${queryString}`,
29
+ + path: `${vocabPath}?${queryString}`,
30
+ },
31
+ subrequest,
32
+ };
@@ -0,0 +1,4 @@
1
+ Backport vocabularies actions from Volto 19.x. See:
2
+
3
+ - https://github.com/plone/volto/pull/6935
4
+ - https://github.com/plone/volto/issues/3216
@@ -0,0 +1,57 @@
1
+ import { getVocabulary } from './vocabularies';
2
+ import { GET_VOCABULARY } from '@plone/volto/constants/ActionTypes';
3
+
4
+ describe('Vocabularies actions', () => {
5
+ describe('getVocabulary', () => {
6
+ it('should create an action to get a vocabulary', () => {
7
+ const vocabNameOrURL = 'plone.app.vocabularies.Keywords';
8
+ const query = 'john';
9
+ const action = getVocabulary({ vocabNameOrURL, query });
10
+
11
+ expect(action.type).toEqual(GET_VOCABULARY);
12
+ expect(action.vocabulary).toEqual(vocabNameOrURL);
13
+ expect(action.request.op).toEqual('get');
14
+ expect(action.request.path).toEqual(
15
+ `/@vocabularies/${vocabNameOrURL}?b_start=0&title=${query}`,
16
+ );
17
+ });
18
+ it('should create an action to get a vocabulary if a URL is passed', () => {
19
+ const vocabNameOrURL =
20
+ 'http://localhost:3000/@vocabularies/plone.app.vocabularies.Keywords';
21
+ const query = 'john';
22
+ const action = getVocabulary({ vocabNameOrURL, query });
23
+
24
+ expect(action.type).toEqual(GET_VOCABULARY);
25
+ expect(action.vocabulary).toEqual(vocabNameOrURL);
26
+ expect(action.request.op).toEqual('get');
27
+ expect(action.request.path).toEqual(
28
+ `/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&title=${query}`,
29
+ );
30
+ });
31
+ it('should create an action to get a vocabulary if a URL with path is passed', () => {
32
+ const vocabNameOrURL =
33
+ 'http://localhost:3000/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords';
34
+ const query = 'john';
35
+ const action = getVocabulary({ vocabNameOrURL, query });
36
+
37
+ expect(action.type).toEqual(GET_VOCABULARY);
38
+ expect(action.vocabulary).toEqual(vocabNameOrURL);
39
+ expect(action.request.op).toEqual('get');
40
+ expect(action.request.path).toEqual(
41
+ `/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&title=${query}`,
42
+ );
43
+ });
44
+ it('should create an action to get a vocabulary if an b_size=-1 is passed', () => {
45
+ const vocabNameOrURL =
46
+ 'http://localhost:3000/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords';
47
+ const action = getVocabulary({ vocabNameOrURL, size: -1 });
48
+
49
+ expect(action.type).toEqual(GET_VOCABULARY);
50
+ expect(action.vocabulary).toEqual(vocabNameOrURL);
51
+ expect(action.request.op).toEqual('get');
52
+ expect(action.request.path).toEqual(
53
+ `/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&b_size=-1`,
54
+ );
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,45 @@
1
+ diff --git a/src/customizations/volto/actions/vocabularies/vocabularies.test.js b/src/customizations/volto/actions/vocabularies/vocabularies.test.js
2
+ index b7deafc..ca4fc8a 100644
3
+ --- a/src/customizations/volto/actions/vocabularies/vocabularies.test.js
4
+ +++ b/src/customizations/volto/actions/vocabularies/vocabularies.test.js
5
+ @@ -17,7 +17,7 @@ describe('Vocabularies actions', () => {
6
+ });
7
+ it('should create an action to get a vocabulary if a URL is passed', () => {
8
+ const vocabNameOrURL =
9
+ - 'http://localhost:8080/@vocabularies/plone.app.vocabularies.Keywords';
10
+ + 'http://localhost:3000/@vocabularies/plone.app.vocabularies.Keywords';
11
+ const query = 'john';
12
+ const action = getVocabulary({ vocabNameOrURL, query });
13
+
14
+ @@ -30,7 +30,7 @@ describe('Vocabularies actions', () => {
15
+ });
16
+ it('should create an action to get a vocabulary if a URL with path is passed', () => {
17
+ const vocabNameOrURL =
18
+ - 'http://localhost:8080/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords';
19
+ + 'http://localhost:3000/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords';
20
+ const query = 'john';
21
+ const action = getVocabulary({ vocabNameOrURL, query });
22
+
23
+ @@ -38,19 +38,19 @@ describe('Vocabularies actions', () => {
24
+ expect(action.vocabulary).toEqual(vocabNameOrURL);
25
+ expect(action.request.op).toEqual('get');
26
+ expect(action.request.path).toEqual(
27
+ - `/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&title=${query}`,
28
+ + `/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&title=${query}`,
29
+ );
30
+ });
31
+ it('should create an action to get a vocabulary if an b_size=-1 is passed', () => {
32
+ const vocabNameOrURL =
33
+ - 'http://localhost:8080/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords';
34
+ + 'http://localhost:3000/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords';
35
+ const action = getVocabulary({ vocabNameOrURL, size: -1 });
36
+
37
+ expect(action.type).toEqual(GET_VOCABULARY);
38
+ expect(action.vocabulary).toEqual(vocabNameOrURL);
39
+ expect(action.request.op).toEqual('get');
40
+ expect(action.request.path).toEqual(
41
+ - `/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&b_size=-1`,
42
+ + `/de/foo/bar/@vocabularies/plone.app.vocabularies.Keywords?b_start=0&b_size=-1`,
43
+ );
44
+ });
45
+ });
@@ -53,6 +53,7 @@ const View = ({ data, properties }) => {
53
53
  )}
54
54
  alt={properties.image_caption || ''}
55
55
  responsive={true}
56
+ loading="lazy"
56
57
  />
57
58
  <div
58
59
  className={`copyright-wrapper ${
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import configureStore from 'redux-mock-store';
4
+ import thunk from 'redux-thunk'; // ✅ Add redux-thunk middleware
5
+ import { render } from '@testing-library/react';
6
+ import withRootNavigation from './withRootNavigation';
7
+ import { getNavigation } from '@plone/volto/actions';
8
+ import { getBaseUrl, hasApiExpander } from '@plone/volto/helpers';
9
+ import config from '@plone/volto/registry';
10
+
11
+ // Mock dependencies
12
+ jest.mock('@plone/volto/actions', () => ({
13
+ getNavigation: jest.fn(() => ({ type: 'GET_NAVIGATION' })), // ✅ Ensure it returns a plain object
14
+ }));
15
+ jest.mock('@plone/volto/helpers', () => ({
16
+ getBaseUrl: jest.fn(() => '/en'),
17
+ hasApiExpander: jest.fn(() => false),
18
+ }));
19
+ jest.mock('@plone/volto/registry', () => ({
20
+ settings: { navDepth: 2 },
21
+ }));
22
+
23
+ // ✅ Use redux-thunk middleware
24
+ const mockStore = configureStore([thunk]); // Add thunk to support async actions
25
+
26
+ const initialState = {
27
+ navigation: { items: [{ title: 'Home', url: '/' }] },
28
+ intl: { locale: 'en' },
29
+ };
30
+
31
+ const store = mockStore(initialState);
32
+
33
+ // Mock Wrapped Component
34
+ const MockComponent = (props) => {
35
+ return (
36
+ <div data-testid="wrapped-component">{JSON.stringify(props.items)}</div>
37
+ );
38
+ };
39
+
40
+ const WrappedComponent = withRootNavigation(MockComponent);
41
+
42
+ describe('withRootNavigation HOC', () => {
43
+ beforeEach(() => {
44
+ jest.clearAllMocks();
45
+ });
46
+
47
+ test('calls getNavigation when API expander is not set', () => {
48
+ render(
49
+ <Provider store={store}>
50
+ <WrappedComponent />
51
+ </Provider>,
52
+ );
53
+
54
+ expect(getBaseUrl).toHaveBeenCalledWith('/en'); // Check base URL calculation
55
+ expect(hasApiExpander).toHaveBeenCalledWith('navigation', '/en'); // Ensure API expander is checked
56
+ expect(getNavigation).toHaveBeenCalledWith('/en', config.settings.navDepth); // Ensure getNavigation is dispatched
57
+ });
58
+
59
+ test('does not call getNavigation if API expander is already set', () => {
60
+ hasApiExpander.mockReturnValue(true); // Simulate that API expander is already set
61
+
62
+ render(
63
+ <Provider store={store}>
64
+ <WrappedComponent />
65
+ </Provider>,
66
+ );
67
+
68
+ expect(getNavigation).not.toHaveBeenCalled(); // Ensure getNavigation is NOT called
69
+ });
70
+ });
package/src/index.js CHANGED
@@ -2,7 +2,6 @@ import React from 'react';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import { Icon } from '@plone/volto/components';
4
4
  import { default as TokenWidgetEdit } from '@plone/volto/components/manage/Widgets/TokenWidget';
5
- import SelectAutoCompleteWidget from '@plone/volto/components/manage/Widgets/SelectAutoComplete';
6
5
  import { serializeNodesToText } from '@plone/volto-slate/editor/render';
7
6
  import TableBlockEdit from '@plone/volto-slate/blocks/Table/TableBlockEdit';
8
7
  import TableBlockView from '@plone/volto-slate/blocks/Table/TableBlockView';
@@ -20,7 +19,10 @@ import { TopicsWidget } from '@eeacms/volto-eea-website-theme/components/theme/W
20
19
  import { DateWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/DateWidget';
21
20
  import { DatetimeWidget } from '@eeacms/volto-eea-website-theme/components/theme/Widgets/DatetimeWidget';
22
21
  import CreatableSelectWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/CreatableSelectWidget';
22
+ import UserSelectWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/UserSelectWidget';
23
23
  import ImageViewWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/ImageViewWidget';
24
+ import CreatorsViewWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/CreatorsViewWidget';
25
+ import ContributorsViewWidget from '@eeacms/volto-eea-website-theme/components/theme/Widgets/ContributorsViewWidget';
24
26
 
25
27
  import Tag from '@eeacms/volto-eea-design-system/ui/Tag/Tag';
26
28
 
@@ -336,6 +338,7 @@ const applyConfig = (config) => {
336
338
  title: 'No. of columns',
337
339
  description: 'Choose the number of flex columns',
338
340
  choices: [
341
+ [1, 1],
339
342
  [2, 2],
340
343
  [3, 3],
341
344
  [4, 4],
@@ -371,9 +374,12 @@ const applyConfig = (config) => {
371
374
  config.widgets.views.id.topics = TopicsWidget;
372
375
  config.widgets.views.id.subjects = TokenWidget;
373
376
  config.widgets.views.widget.tags = TokenWidget;
377
+ config.widgets.views.id.creators = CreatorsViewWidget;
378
+ config.widgets.views.id.contributors = ContributorsViewWidget;
379
+ config.widgets.views.widget.contributors = ContributorsViewWidget;
380
+ config.widgets.views.widget.creators = CreatorsViewWidget;
374
381
  config.widgets.widget.creatable_select = CreatableSelectWidget;
375
- config.widgets.vocabulary['plone.app.vocabularies.Users'] =
376
- SelectAutoCompleteWidget;
382
+ config.widgets.vocabulary['plone.app.vocabularies.Users'] = UserSelectWidget;
377
383
 
378
384
  config.widgets.views.factory = {
379
385
  ...(config.widgets.views.factory || {}),
package/src/index.test.js CHANGED
@@ -46,6 +46,12 @@ jest.mock('@plone/volto/components', () => ({
46
46
  Icon: 'MockedIcon',
47
47
  }));
48
48
 
49
+ jest.mock('uuid', () => {
50
+ return {
51
+ v4: () => 'mock-uuid-' + Math.random().toString(36).substr(2, 9),
52
+ };
53
+ });
54
+
49
55
  global.__SERVER__ = true;
50
56
 
51
57
  describe('applyConfig', () => {