@eeacms/volto-eea-website-theme 3.5.4 → 3.6.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.
@@ -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