@eeacms/volto-eea-website-theme 1.32.0 → 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 +28 -1
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/components/manage/Blocks/LayoutSettings/LayoutSettingsView.test.jsx +68 -0
- package/src/components/manage/Blocks/LayoutSettings/schema.js +1 -0
- package/src/components/theme/Widgets/CreatableSelectWidget.jsx +304 -0
- package/src/components/theme/Widgets/CreatableSelectWidget.test.jsx +89 -0
- package/src/customizations/@plone/volto-slate/elementEditor/utils.js +245 -0
- package/src/customizations/volto/components/manage/Blocks/Image/View.jsx +1 -1
- package/src/helpers/schema-utils.js +7 -1
- package/src/index.js +3 -21
- package/src/index.test.js +2 -0
package/CHANGELOG.md
CHANGED
@@ -4,13 +4,36 @@ 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.
|
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)]
|
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
|
10
29
|
|
11
30
|
- feat(slate): add new h3 slate element [Miu Razvan - [`20a7e93`](https://github.com/eea/volto-eea-website-theme/commit/20a7e9316d5c1de279712c38bc05eb775f8bd326)]
|
12
31
|
- feat(slate): add h4 in slate toolbar [Miu Razvan - [`2c523ac`](https://github.com/eea/volto-eea-website-theme/commit/2c523acdeb78ed224c4a37285c48dbf4555604e0)]
|
13
32
|
|
33
|
+
#### :bug: Bug Fixes
|
34
|
+
|
35
|
+
- fix(slate): insert/remove element fix, refs #261770 [Miu Razvan - [`b121ffd`](https://github.com/eea/volto-eea-website-theme/commit/b121ffd2de5c5973f9819b949be1c83400d72edb)]
|
36
|
+
|
14
37
|
#### :nail_care: Enhancements
|
15
38
|
|
16
39
|
- 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)]
|
@@ -23,8 +46,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
23
46
|
|
24
47
|
#### :hammer_and_wrench: Others
|
25
48
|
|
49
|
+
- fix conflicts [Miu Razvan - [`9ad80fc`](https://github.com/eea/volto-eea-website-theme/commit/9ad80fc98a3057d96f573150456bc43b5f822782)]
|
50
|
+
- fix conflicts [Miu Razvan - [`dee94db`](https://github.com/eea/volto-eea-website-theme/commit/dee94db33c47a2d963ea912712723768de547719)]
|
26
51
|
- Update package.json [ichim-david - [`9db96ff`](https://github.com/eea/volto-eea-website-theme/commit/9db96ff3f8e8eedce92c53befe6a1d6e965d8699)]
|
27
52
|
- 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)]
|
53
|
+
### [1.32.0](https://github.com/eea/volto-eea-website-theme/compare/1.31.0...1.32.0) - 25 March 2024
|
54
|
+
|
28
55
|
### [1.31.0](https://github.com/eea/volto-eea-website-theme/compare/1.30.0...1.31.0) - 14 March 2024
|
29
56
|
|
30
57
|
#### :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
@@ -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
|
+
});
|
@@ -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
|
+
});
|
@@ -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
|
+
};
|
@@ -20,7 +20,7 @@ import { Copyright } from '@eeacms/volto-eea-design-system/ui';
|
|
20
20
|
*/
|
21
21
|
export const View = (props) => {
|
22
22
|
const { data, detached } = props;
|
23
|
-
const href = data?.href?.[0]?.['@id']
|
23
|
+
const href = data?.href?.[0]?.['@id'] || '';
|
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
|
-
|
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: {},
|