@plone/volto 14.0.2 → 14.2.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 +46 -0
- package/README.md +24 -3
- package/locales/ca/LC_MESSAGES/volto.po +12 -2
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +12 -2
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +12 -2
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +12 -2
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +12 -2
- package/locales/eu.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +12 -2
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +12 -2
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +12 -2
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +12 -2
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +12 -2
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +12 -2
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +12 -2
- package/locales/ro.json +1 -1
- package/locales/volto.pot +12 -2
- package/package.json +3 -2
- package/src/actions/vocabularies/vocabularies.js +15 -3
- package/src/components/index.js +1 -0
- package/src/components/manage/Add/Add.jsx +1 -0
- package/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +1 -1
- package/src/components/manage/Blocks/Listing/getAsyncData.js +1 -1
- package/src/components/manage/Blocks/Text/Edit.jsx +19 -0
- package/src/components/manage/Edit/Edit.jsx +1 -0
- package/src/components/manage/Form/Form.jsx +32 -6
- package/src/components/manage/Form/UndoToolbar.jsx +78 -0
- package/src/components/manage/Multilingual/TranslationObject.jsx +1 -0
- package/src/components/manage/Widgets/AlignWidget.stories.jsx +5 -21
- package/src/components/manage/Widgets/ArrayWidget.jsx +88 -88
- package/src/components/manage/Widgets/ArrayWidget.stories.jsx +37 -64
- package/src/components/manage/Widgets/CheckboxWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/DatetimeWidget.jsx +65 -74
- package/src/components/manage/Widgets/DatetimeWidget.stories.jsx +7 -23
- package/src/components/manage/Widgets/DatetimeWidget.test.jsx +17 -15
- package/src/components/manage/Widgets/EmailWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/FileWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/NumberWidget.stories.jsx +5 -23
- package/src/components/manage/Widgets/ObjectBrowserWidget.stories.js +24 -32
- package/src/components/manage/Widgets/ObjectListWidget.stories.js +44 -44
- package/src/components/manage/Widgets/ObjectWidget.stories.jsx +13 -28
- package/src/components/manage/Widgets/PasswordWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/QueryWidget.jsx +2 -2
- package/src/components/manage/Widgets/QueryWidget.stories.jsx +1637 -22
- package/src/components/manage/Widgets/SelectAutoComplete.jsx +79 -48
- package/src/components/manage/Widgets/SelectAutoComplete.test.jsx +16 -0
- package/src/components/manage/Widgets/SelectAutocompleteWidget.stories.jsx +161 -0
- package/src/components/manage/Widgets/SelectUtils.js +90 -30
- package/src/components/manage/Widgets/SelectUtils.test.jsx +76 -1
- package/src/components/manage/Widgets/SelectWidget.jsx +26 -37
- package/src/components/manage/Widgets/SelectWidget.stories.jsx +96 -28
- package/src/components/manage/Widgets/TextWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/TextareaWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/TokenWidget.jsx +19 -17
- package/src/components/manage/Widgets/TokenWidget.stories.jsx +141 -0
- package/src/components/manage/Widgets/UrlWidget.stories.jsx +5 -21
- package/src/components/manage/Widgets/VocabularyTermsWidget.stories.js +27 -64
- package/src/components/manage/Widgets/WysiwygWidget.stories.jsx +5 -22
- package/src/components/manage/Widgets/story.jsx +38 -0
- package/src/components/theme/ContactForm/ContactForm.jsx +1 -0
- package/src/components/theme/ContactForm/ContactForm.stories.jsx +126 -0
- package/src/components/theme/CorsError/CorsError.jsx +2 -2
- package/src/components/theme/Navigation/NavItem.jsx +3 -1
- package/src/config/Blocks.jsx +8 -1
- package/src/config/Loadables.jsx +2 -0
- package/src/config/index.js +3 -0
- package/src/helpers/UndoManager/useUndoManager.js +102 -0
- package/src/helpers/index.js +1 -0
- package/src/reducers/vocabularies/vocabularies.js +13 -2
- package/src/store.js +1 -1
- package/src/storybook.jsx +55 -0
- package/theme/themes/pastanaga/extras/time-picker-overrides.less +1 -1
- package/include/python3.8/Python-ast.h +0 -715
- package/include/python3.8/Python.h +0 -160
- package/include/python3.8/abstract.h +0 -844
- package/include/python3.8/asdl.h +0 -46
- package/include/python3.8/ast.h +0 -37
- package/include/python3.8/bitset.h +0 -23
- package/include/python3.8/bltinmodule.h +0 -14
- package/include/python3.8/boolobject.h +0 -34
- package/include/python3.8/bytearrayobject.h +0 -62
- package/include/python3.8/bytes_methods.h +0 -69
- package/include/python3.8/bytesobject.h +0 -224
- package/include/python3.8/cellobject.h +0 -29
- package/include/python3.8/ceval.h +0 -231
- package/include/python3.8/classobject.h +0 -59
- package/include/python3.8/code.h +0 -180
- package/include/python3.8/codecs.h +0 -240
- package/include/python3.8/compile.h +0 -106
- package/include/python3.8/complexobject.h +0 -69
- package/include/python3.8/context.h +0 -84
- package/include/python3.8/cpython/abstract.h +0 -319
- package/include/python3.8/cpython/dictobject.h +0 -94
- package/include/python3.8/cpython/fileobject.h +0 -24
- package/include/python3.8/cpython/initconfig.h +0 -434
- package/include/python3.8/cpython/interpreteridobject.h +0 -19
- package/include/python3.8/cpython/object.h +0 -470
- package/include/python3.8/cpython/objimpl.h +0 -113
- package/include/python3.8/cpython/pyerrors.h +0 -188
- package/include/python3.8/cpython/pylifecycle.h +0 -78
- package/include/python3.8/cpython/pymem.h +0 -108
- package/include/python3.8/cpython/pystate.h +0 -252
- package/include/python3.8/cpython/sysmodule.h +0 -21
- package/include/python3.8/cpython/traceback.h +0 -22
- package/include/python3.8/cpython/tupleobject.h +0 -36
- package/include/python3.8/cpython/unicodeobject.h +0 -1239
- package/include/python3.8/datetime.h +0 -259
- package/include/python3.8/descrobject.h +0 -108
- package/include/python3.8/dictobject.h +0 -94
- package/include/python3.8/dtoa.h +0 -19
- package/include/python3.8/dynamic_annotations.h +0 -499
- package/include/python3.8/enumobject.h +0 -17
- package/include/python3.8/errcode.h +0 -38
- package/include/python3.8/eval.h +0 -37
- package/include/python3.8/fileobject.h +0 -49
- package/include/python3.8/fileutils.h +0 -185
- package/include/python3.8/floatobject.h +0 -130
- package/include/python3.8/frameobject.h +0 -92
- package/include/python3.8/funcobject.h +0 -104
- package/include/python3.8/genobject.h +0 -109
- package/include/python3.8/graminit.h +0 -94
- package/include/python3.8/grammar.h +0 -77
- package/include/python3.8/import.h +0 -149
- package/include/python3.8/internal/pycore_accu.h +0 -39
- package/include/python3.8/internal/pycore_atomic.h +0 -558
- package/include/python3.8/internal/pycore_ceval.h +0 -37
- package/include/python3.8/internal/pycore_code.h +0 -27
- package/include/python3.8/internal/pycore_condvar.h +0 -95
- package/include/python3.8/internal/pycore_context.h +0 -42
- package/include/python3.8/internal/pycore_fileutils.h +0 -54
- package/include/python3.8/internal/pycore_getopt.h +0 -22
- package/include/python3.8/internal/pycore_gil.h +0 -50
- package/include/python3.8/internal/pycore_hamt.h +0 -116
- package/include/python3.8/internal/pycore_initconfig.h +0 -166
- package/include/python3.8/internal/pycore_object.h +0 -81
- package/include/python3.8/internal/pycore_pathconfig.h +0 -75
- package/include/python3.8/internal/pycore_pyerrors.h +0 -62
- package/include/python3.8/internal/pycore_pyhash.h +0 -10
- package/include/python3.8/internal/pycore_pylifecycle.h +0 -118
- package/include/python3.8/internal/pycore_pymem.h +0 -212
- package/include/python3.8/internal/pycore_pystate.h +0 -326
- package/include/python3.8/internal/pycore_traceback.h +0 -96
- package/include/python3.8/internal/pycore_tupleobject.h +0 -19
- package/include/python3.8/internal/pycore_warnings.h +0 -25
- package/include/python3.8/interpreteridobject.h +0 -17
- package/include/python3.8/intrcheck.h +0 -33
- package/include/python3.8/iterobject.h +0 -25
- package/include/python3.8/listobject.h +0 -81
- package/include/python3.8/longintrepr.h +0 -99
- package/include/python3.8/longobject.h +0 -242
- package/include/python3.8/marshal.h +0 -28
- package/include/python3.8/memoryobject.h +0 -72
- package/include/python3.8/methodobject.h +0 -131
- package/include/python3.8/modsupport.h +0 -248
- package/include/python3.8/moduleobject.h +0 -90
- package/include/python3.8/namespaceobject.h +0 -19
- package/include/python3.8/node.h +0 -48
- package/include/python3.8/object.h +0 -753
- package/include/python3.8/objimpl.h +0 -284
- package/include/python3.8/odictobject.h +0 -43
- package/include/python3.8/opcode.h +0 -148
- package/include/python3.8/osdefs.h +0 -51
- package/include/python3.8/osmodule.h +0 -17
- package/include/python3.8/parsetok.h +0 -110
- package/include/python3.8/patchlevel.h +0 -35
- package/include/python3.8/picklebufobject.h +0 -31
- package/include/python3.8/py_curses.h +0 -100
- package/include/python3.8/pyarena.h +0 -64
- package/include/python3.8/pycapsule.h +0 -59
- package/include/python3.8/pyconfig.h +0 -1665
- package/include/python3.8/pyctype.h +0 -39
- package/include/python3.8/pydebug.h +0 -40
- package/include/python3.8/pydtrace.h +0 -59
- package/include/python3.8/pydtrace_probes.h +0 -228
- package/include/python3.8/pyerrors.h +0 -335
- package/include/python3.8/pyexpat.h +0 -55
- package/include/python3.8/pyfpe.h +0 -12
- package/include/python3.8/pyhash.h +0 -145
- package/include/python3.8/pylifecycle.h +0 -75
- package/include/python3.8/pymacconfig.h +0 -102
- package/include/python3.8/pymacro.h +0 -106
- package/include/python3.8/pymath.h +0 -230
- package/include/python3.8/pymem.h +0 -150
- package/include/python3.8/pyport.h +0 -850
- package/include/python3.8/pystate.h +0 -136
- package/include/python3.8/pystrcmp.h +0 -23
- package/include/python3.8/pystrhex.h +0 -22
- package/include/python3.8/pystrtod.h +0 -45
- package/include/python3.8/pythonrun.h +0 -210
- package/include/python3.8/pythread.h +0 -161
- package/include/python3.8/pytime.h +0 -246
- package/include/python3.8/rangeobject.h +0 -27
- package/include/python3.8/setobject.h +0 -108
- package/include/python3.8/sliceobject.h +0 -65
- package/include/python3.8/structmember.h +0 -74
- package/include/python3.8/structseq.h +0 -49
- package/include/python3.8/symtable.h +0 -123
- package/include/python3.8/sysmodule.h +0 -41
- package/include/python3.8/token.h +0 -92
- package/include/python3.8/traceback.h +0 -28
- package/include/python3.8/tracemalloc.h +0 -38
- package/include/python3.8/tupleobject.h +0 -48
- package/include/python3.8/typeslots.h +0 -85
- package/include/python3.8/ucnhash.h +0 -36
- package/include/python3.8/unicodeobject.h +0 -1044
- package/include/python3.8/warnings.h +0 -67
- package/include/python3.8/weakrefobject.h +0 -86
- package/src/components/theme/ContactForm/ContactForm.stories.mdx +0 -39
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @module components/manage/Widgets/
|
|
2
|
+
* SelectAutoComplete component.
|
|
3
|
+
* @module components/manage/Widgets/SelectAutoComplete
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { Component } from 'react';
|
|
7
7
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
8
8
|
import PropTypes from 'prop-types';
|
|
9
|
-
import { isObject } from 'lodash';
|
|
10
9
|
import { compose } from 'redux';
|
|
11
10
|
import { connect } from 'react-redux';
|
|
12
11
|
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
12
|
+
import {
|
|
13
|
+
normalizeValue,
|
|
14
|
+
normalizeChoices,
|
|
15
|
+
convertValueToVocabQuery,
|
|
16
|
+
} from './SelectUtils';
|
|
13
17
|
|
|
14
18
|
import {
|
|
15
19
|
getVocabFromHint,
|
|
16
20
|
getVocabFromField,
|
|
17
21
|
getVocabFromItems,
|
|
18
22
|
} from '@plone/volto/helpers';
|
|
19
|
-
import { getVocabulary } from '@plone/volto/actions';
|
|
23
|
+
import { getVocabulary, getVocabularyTokenTitle } from '@plone/volto/actions';
|
|
20
24
|
|
|
21
25
|
import {
|
|
22
26
|
Option,
|
|
27
|
+
ClearIndicator,
|
|
23
28
|
DropdownIndicator,
|
|
24
29
|
selectTheme,
|
|
25
30
|
customSelectStyles,
|
|
@@ -44,8 +49,8 @@ const messages = defineMessages({
|
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
/**
|
|
47
|
-
*
|
|
48
|
-
* @class
|
|
52
|
+
* SelectAutoComplete component class.
|
|
53
|
+
* @class SelectAutoComplete
|
|
49
54
|
* @extends Component
|
|
50
55
|
*/
|
|
51
56
|
class SelectAutoComplete extends Component {
|
|
@@ -75,6 +80,7 @@ class SelectAutoComplete extends Component {
|
|
|
75
80
|
),
|
|
76
81
|
onChange: PropTypes.func.isRequired,
|
|
77
82
|
wrapped: PropTypes.bool,
|
|
83
|
+
isDisabled: PropTypes.bool,
|
|
78
84
|
};
|
|
79
85
|
|
|
80
86
|
/**
|
|
@@ -108,17 +114,39 @@ class SelectAutoComplete extends Component {
|
|
|
108
114
|
this.handleChange = this.handleChange.bind(this);
|
|
109
115
|
|
|
110
116
|
this.state = {
|
|
111
|
-
selectedOption: props.value
|
|
112
|
-
? props.value.map((item) =>
|
|
113
|
-
isObject(item)
|
|
114
|
-
? { label: item.title || item.token, value: item.token }
|
|
115
|
-
: { label: item, value: item },
|
|
116
|
-
)
|
|
117
|
-
: [],
|
|
118
117
|
searchLength: 0,
|
|
118
|
+
termsPairsCache: [],
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
componentDidMount() {
|
|
123
|
+
const { id, intl, value, choices } = this.props;
|
|
124
|
+
if (value && value?.length > 0) {
|
|
125
|
+
const tokensQuery = convertValueToVocabQuery(
|
|
126
|
+
normalizeValue(choices, value, this.props.intl),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
this.props.getVocabularyTokenTitle({
|
|
130
|
+
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
131
|
+
subrequest: `widget-${id}-${intl.locale}`,
|
|
132
|
+
...tokensQuery,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
componentDidUpdate(prevProps, prevState) {
|
|
138
|
+
const { value, choices = [] } = this.props;
|
|
139
|
+
if (
|
|
140
|
+
this.state.termsPairsCache.length === 0 &&
|
|
141
|
+
value?.length > 0 &&
|
|
142
|
+
choices.length > 0
|
|
143
|
+
) {
|
|
144
|
+
this.setState((state) => ({
|
|
145
|
+
termsPairsCache: [...state.termsPairsCache, ...choices],
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
122
150
|
/**
|
|
123
151
|
* Handle the field change, store it in the local state and back to simple
|
|
124
152
|
* array of tokens for correct serialization
|
|
@@ -127,12 +155,13 @@ class SelectAutoComplete extends Component {
|
|
|
127
155
|
* @returns {undefined}
|
|
128
156
|
*/
|
|
129
157
|
handleChange(selectedOption) {
|
|
130
|
-
this.setState({ selectedOption });
|
|
131
|
-
|
|
132
158
|
this.props.onChange(
|
|
133
159
|
this.props.id,
|
|
134
160
|
selectedOption ? selectedOption.map((item) => item.value) : null,
|
|
135
161
|
);
|
|
162
|
+
this.setState((state) => ({
|
|
163
|
+
termsPairsCache: [...state.termsPairsCache, ...selectedOption],
|
|
164
|
+
}));
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
timeoutRef = React.createRef();
|
|
@@ -146,21 +175,8 @@ class SelectAutoComplete extends Component {
|
|
|
146
175
|
if (this.timeoutRef.current) clearTimeout(this.timeoutRef.current);
|
|
147
176
|
return new Promise((resolve) => {
|
|
148
177
|
this.timeoutRef.current = setTimeout(async () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.getVocabulary({
|
|
152
|
-
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
153
|
-
query,
|
|
154
|
-
size: -1,
|
|
155
|
-
subrequest: this.props.intl.locale,
|
|
156
|
-
})
|
|
157
|
-
.then((resp) => {
|
|
158
|
-
return resp.items.map((item) => ({
|
|
159
|
-
label: item.title,
|
|
160
|
-
value: item.token,
|
|
161
|
-
}));
|
|
162
|
-
}),
|
|
163
|
-
);
|
|
178
|
+
const res = await this.fetchAvailableChoices(query);
|
|
179
|
+
resolve(res);
|
|
164
180
|
}, 400);
|
|
165
181
|
});
|
|
166
182
|
} else {
|
|
@@ -168,13 +184,28 @@ class SelectAutoComplete extends Component {
|
|
|
168
184
|
}
|
|
169
185
|
};
|
|
170
186
|
|
|
187
|
+
fetchAvailableChoices = async (query) => {
|
|
188
|
+
const resp = await this.props.getVocabulary({
|
|
189
|
+
vocabNameOrURL: this.props.vocabBaseUrl,
|
|
190
|
+
query,
|
|
191
|
+
size: -1,
|
|
192
|
+
subrequest: this.props.intl.locale,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return normalizeChoices(resp.items || [], this.props.intl);
|
|
196
|
+
};
|
|
197
|
+
|
|
171
198
|
/**
|
|
172
199
|
* Render method.
|
|
173
200
|
* @method render
|
|
174
201
|
* @returns {string} Markup for the component.
|
|
175
202
|
*/
|
|
176
203
|
render() {
|
|
177
|
-
const
|
|
204
|
+
const selectedOption = normalizeValue(
|
|
205
|
+
this.state.termsPairsCache,
|
|
206
|
+
this.props.value,
|
|
207
|
+
this.props.intl,
|
|
208
|
+
);
|
|
178
209
|
const SelectAsync = this.props.reactSelectAsync.default;
|
|
179
210
|
|
|
180
211
|
return (
|
|
@@ -182,7 +213,7 @@ class SelectAutoComplete extends Component {
|
|
|
182
213
|
<SelectAsync
|
|
183
214
|
id={`field-${this.props.id}`}
|
|
184
215
|
key={this.props.id}
|
|
185
|
-
isDisabled={this.props.isDisabled}
|
|
216
|
+
isDisabled={this.props.disabled || this.props.isDisabled}
|
|
186
217
|
className="react-select-container"
|
|
187
218
|
classNamePrefix="react-select"
|
|
188
219
|
cacheOptions
|
|
@@ -204,6 +235,7 @@ class SelectAutoComplete extends Component {
|
|
|
204
235
|
...(this.props.choices?.length > 25 && {
|
|
205
236
|
MenuList,
|
|
206
237
|
}),
|
|
238
|
+
ClearIndicator,
|
|
207
239
|
DropdownIndicator,
|
|
208
240
|
Option,
|
|
209
241
|
}}
|
|
@@ -230,22 +262,21 @@ export default compose(
|
|
|
230
262
|
getVocabFromItems(props);
|
|
231
263
|
|
|
232
264
|
const vocabState =
|
|
233
|
-
state.vocabularies?.[vocabBaseUrl]?.subrequests?.[
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return { vocabBaseUrl };
|
|
265
|
+
state.vocabularies?.[vocabBaseUrl]?.subrequests?.[
|
|
266
|
+
`widget-${props.id}-${props.intl.locale}`
|
|
267
|
+
]?.items;
|
|
268
|
+
|
|
269
|
+
// If the schema already has the choices in it, then do not try to get
|
|
270
|
+
// the vocab, even if there is one
|
|
271
|
+
return props.items?.choices
|
|
272
|
+
? { choices: props.items.choices }
|
|
273
|
+
: vocabState
|
|
274
|
+
? {
|
|
275
|
+
choices: vocabState,
|
|
276
|
+
vocabBaseUrl,
|
|
277
|
+
}
|
|
278
|
+
: { vocabBaseUrl };
|
|
248
279
|
},
|
|
249
|
-
{ getVocabulary },
|
|
280
|
+
{ getVocabulary, getVocabularyTokenTitle },
|
|
250
281
|
),
|
|
251
282
|
)(SelectAutoComplete);
|
|
@@ -27,9 +27,25 @@ test('renders a select widget component', async () => {
|
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
const props = {
|
|
31
|
+
getVocabulary: () => {
|
|
32
|
+
return Promise.resolve({
|
|
33
|
+
items: [
|
|
34
|
+
{ token: 'foo', title: 'Foo' },
|
|
35
|
+
{ token: 'bar', title: 'Bar' },
|
|
36
|
+
{ token: 'fooBar', title: 'FooBar' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
widgetOptions: {
|
|
41
|
+
vocabulary: { '@id': 'plone.app.vocabularies.Keywords' },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
30
45
|
const { container } = render(
|
|
31
46
|
<Provider store={store}>
|
|
32
47
|
<SelectAutoComplete
|
|
48
|
+
{...props}
|
|
33
49
|
id="my-field"
|
|
34
50
|
title="My field"
|
|
35
51
|
fieldSet="default"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { SelectAutoCompleteComponent } from './SelectAutoComplete';
|
|
3
|
+
import WidgetStory from './story';
|
|
4
|
+
|
|
5
|
+
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
6
|
+
|
|
7
|
+
const SelectAutocompleteWidget = injectLazyLibs(['reactSelectAsync'])(
|
|
8
|
+
SelectAutoCompleteComponent,
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const props = {
|
|
12
|
+
choices: [
|
|
13
|
+
{ token: 'foo', title: 'Foo' },
|
|
14
|
+
{ token: 'bar', title: 'Bar' },
|
|
15
|
+
{ token: 'fooBar', title: 'FooBar' },
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
getVocabulary: () => {
|
|
19
|
+
return Promise.resolve({ items: props.choices });
|
|
20
|
+
},
|
|
21
|
+
getVocabularyTokenTitle: () => {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Default = WidgetStory.bind({
|
|
25
|
+
widget: SelectAutocompleteWidget,
|
|
26
|
+
});
|
|
27
|
+
Default.args = {
|
|
28
|
+
...props,
|
|
29
|
+
id: 'field-empty',
|
|
30
|
+
title: 'field 1 title',
|
|
31
|
+
description: 'Optional help text',
|
|
32
|
+
placeholder: 'Type something…',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Required = WidgetStory.bind({
|
|
36
|
+
widget: SelectAutocompleteWidget,
|
|
37
|
+
});
|
|
38
|
+
Required.args = {
|
|
39
|
+
...props,
|
|
40
|
+
id: 'field-empty',
|
|
41
|
+
title: 'field 1 title',
|
|
42
|
+
description: 'Optional help text',
|
|
43
|
+
placeholder: 'Type something…',
|
|
44
|
+
required: true,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const FilledWithToken = WidgetStory.bind({
|
|
48
|
+
widget: SelectAutocompleteWidget,
|
|
49
|
+
});
|
|
50
|
+
FilledWithToken.args = {
|
|
51
|
+
...props,
|
|
52
|
+
id: 'field-filled',
|
|
53
|
+
title: 'Filled field title',
|
|
54
|
+
description: 'Optional help text',
|
|
55
|
+
value: [{ token: 'foo', title: 'Foo' }],
|
|
56
|
+
placeholder: 'Type something…',
|
|
57
|
+
required: true,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const FilledWithString = WidgetStory.bind({
|
|
61
|
+
widget: SelectAutocompleteWidget,
|
|
62
|
+
});
|
|
63
|
+
FilledWithString.args = {
|
|
64
|
+
...props,
|
|
65
|
+
id: 'field-filled',
|
|
66
|
+
title: 'Filled field title',
|
|
67
|
+
description: 'Optional help text',
|
|
68
|
+
value: ['foo'],
|
|
69
|
+
placeholder: 'Type something…',
|
|
70
|
+
required: true,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const Errored = WidgetStory.bind({
|
|
74
|
+
widget: SelectAutocompleteWidget,
|
|
75
|
+
});
|
|
76
|
+
Errored.args = {
|
|
77
|
+
...props,
|
|
78
|
+
id: 'field-errored',
|
|
79
|
+
title: 'Errored field title',
|
|
80
|
+
description: 'Optional help text',
|
|
81
|
+
placeholder: 'Type something…',
|
|
82
|
+
value: [{ token: 'foo', title: 'Foo' }],
|
|
83
|
+
error: ['This is the error'],
|
|
84
|
+
required: true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const NoPlaceholder = WidgetStory.bind({
|
|
88
|
+
widget: SelectAutocompleteWidget,
|
|
89
|
+
});
|
|
90
|
+
NoPlaceholder.args = {
|
|
91
|
+
...props,
|
|
92
|
+
id: 'field-without-novalue',
|
|
93
|
+
title: 'Field title',
|
|
94
|
+
description: 'This field has no value option',
|
|
95
|
+
required: true,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const Disabled = WidgetStory.bind({
|
|
99
|
+
widget: SelectAutocompleteWidget,
|
|
100
|
+
});
|
|
101
|
+
Disabled.args = {
|
|
102
|
+
...props,
|
|
103
|
+
id: 'field-disabled',
|
|
104
|
+
title: 'Disabled field title',
|
|
105
|
+
description: 'This select field is disabled',
|
|
106
|
+
disabled: true,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const getOptionsGenerator = (count) => {
|
|
110
|
+
const options = [];
|
|
111
|
+
for (let i = 0; i < count; i = i + 1) {
|
|
112
|
+
options.push({ token: i.toString(), title: `Option ${i}` });
|
|
113
|
+
}
|
|
114
|
+
return options;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const manyOptions1000 = getOptionsGenerator(1000);
|
|
118
|
+
|
|
119
|
+
export const ManyOptions1000 = WidgetStory.bind({
|
|
120
|
+
widget: SelectAutocompleteWidget,
|
|
121
|
+
});
|
|
122
|
+
ManyOptions1000.args = {
|
|
123
|
+
...props,
|
|
124
|
+
id: 'field-empty',
|
|
125
|
+
title: 'field 1 title',
|
|
126
|
+
description: 'Optional help text',
|
|
127
|
+
placeholder: 'Type something…',
|
|
128
|
+
choices: manyOptions1000.slice(0, 20),
|
|
129
|
+
getVocabulary: () => {
|
|
130
|
+
return Promise.resolve({
|
|
131
|
+
items: manyOptions1000,
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default {
|
|
137
|
+
title: 'Widgets/SelectAutocomplete Widget',
|
|
138
|
+
component: SelectAutoCompleteComponent,
|
|
139
|
+
decorators: [
|
|
140
|
+
(Story) => (
|
|
141
|
+
<div style={{ width: '400px' }}>
|
|
142
|
+
<Story />
|
|
143
|
+
</div>
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
argTypes: {
|
|
147
|
+
// controlled value prop
|
|
148
|
+
value: {
|
|
149
|
+
control: {
|
|
150
|
+
disable: true,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
getVocabulary: {
|
|
154
|
+
control: {
|
|
155
|
+
disable: true,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
// excludeStories: ['searchResults'],
|
|
160
|
+
// subcomponents: { ArgsTable },
|
|
161
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isBoolean, isObject, isString } from 'lodash';
|
|
2
2
|
import { getBoolean } from '@plone/volto/helpers';
|
|
3
3
|
import { defineMessages } from 'react-intl';
|
|
4
4
|
|
|
@@ -9,6 +9,67 @@ const messages = defineMessages({
|
|
|
9
9
|
},
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Prepares a vocab endpoint query for tokens based on passed value.
|
|
14
|
+
*
|
|
15
|
+
* This can be used to facilitate querying a vocabulary endpoint for labels,
|
|
16
|
+
* given some token values. This assumes that the value has already been
|
|
17
|
+
* normalized by normalizeValue.
|
|
18
|
+
*/
|
|
19
|
+
export function convertValueToVocabQuery(value) {
|
|
20
|
+
if (isString(value) || isBoolean(value)) return { token: value.toString() };
|
|
21
|
+
|
|
22
|
+
if (!value) return {};
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return {
|
|
26
|
+
tokens: value
|
|
27
|
+
.map((v) =>
|
|
28
|
+
isObject(v)
|
|
29
|
+
? v.value ?? v.token
|
|
30
|
+
: isString(v) || isBoolean(v)
|
|
31
|
+
? v
|
|
32
|
+
: null,
|
|
33
|
+
)
|
|
34
|
+
.filter((f) => f !== null),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const token = value.value ?? value.token;
|
|
39
|
+
return isString(token) ? { token } : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes provided value to a "best representation" value, as accepted by
|
|
44
|
+
* react-select. In this case, it is an object of shape `{ label, value }`
|
|
45
|
+
*/
|
|
46
|
+
export function normalizeSingleSelectOption(value, intl) {
|
|
47
|
+
if (!value) return value;
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
// assuming [token, title] pair.
|
|
51
|
+
if (value.length === 2)
|
|
52
|
+
return { value: value[0], label: value[1] || value[0] };
|
|
53
|
+
|
|
54
|
+
throw new Error(`Unknown value type of select widget: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const token = value.token ?? value.value ?? 'no-value';
|
|
58
|
+
const label =
|
|
59
|
+
(value.title && value.title !== 'None' ? value.title : undefined) ??
|
|
60
|
+
value.label ??
|
|
61
|
+
value.token ??
|
|
62
|
+
intl.formatMessage(messages.no_value);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
value: token,
|
|
66
|
+
label,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const normalizeChoices = (items, intl) =>
|
|
71
|
+
items.map((item) => normalizeSingleSelectOption(item, intl));
|
|
72
|
+
|
|
12
73
|
/**
|
|
13
74
|
* Given the value from the API, it normalizes to a value valid to use in react-select.
|
|
14
75
|
* This is necessary because of the inconsistencies in p.restapi vocabularies implementations as
|
|
@@ -16,50 +77,49 @@ const messages = defineMessages({
|
|
|
16
77
|
* @function normalizeValue
|
|
17
78
|
* @param {array} choices The choices
|
|
18
79
|
* @param {string|object|boolean|array} value The value
|
|
19
|
-
* @returns {Object} An object of shape {label: "", value: ""}
|
|
80
|
+
* @returns {Object} An object of shape {label: "", value: ""} (or an array)
|
|
20
81
|
*/
|
|
21
|
-
export function normalizeValue(choices, value) {
|
|
82
|
+
export function normalizeValue(choices, value, intl) {
|
|
83
|
+
choices = normalizeChoices(choices || [], intl);
|
|
84
|
+
const choiceMap = Object.assign(
|
|
85
|
+
{},
|
|
86
|
+
...choices.map(({ label, value }) => ({
|
|
87
|
+
[value]: label,
|
|
88
|
+
})),
|
|
89
|
+
);
|
|
90
|
+
|
|
22
91
|
if (!isObject(value) && isBoolean(value)) {
|
|
23
92
|
// We have a boolean value, which means we need to provide a "No value"
|
|
24
93
|
// option
|
|
25
|
-
const label =
|
|
94
|
+
const label = choiceMap[getBoolean(value)];
|
|
26
95
|
return label
|
|
27
96
|
? {
|
|
28
|
-
label
|
|
97
|
+
label,
|
|
29
98
|
value,
|
|
30
99
|
}
|
|
31
100
|
: {};
|
|
32
101
|
}
|
|
33
|
-
if (value === undefined) return null;
|
|
34
|
-
if (!value || value.length === 0) return null;
|
|
35
102
|
if (value === 'no-value') {
|
|
36
103
|
return {
|
|
37
|
-
label:
|
|
104
|
+
label: intl.formatMessage(messages.no_value),
|
|
38
105
|
value: 'no-value',
|
|
39
106
|
};
|
|
40
107
|
}
|
|
41
108
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} else if (isObject(value)) {
|
|
48
|
-
return {
|
|
49
|
-
label: value.title !== 'None' && value.title ? value.title : value.token,
|
|
50
|
-
value: value.token,
|
|
51
|
-
};
|
|
52
|
-
} else if (value && choices && choices.length > 0 && isArray(choices[0])) {
|
|
53
|
-
return { label: find(choices, (o) => o[0] === value)?.[1] || value, value };
|
|
54
|
-
} else if (
|
|
55
|
-
value &&
|
|
56
|
-
choices &&
|
|
57
|
-
choices.length > 0 &&
|
|
58
|
-
Object.keys(choices[0]).includes('value') &&
|
|
59
|
-
Object.keys(choices[0]).includes('label')
|
|
60
|
-
) {
|
|
61
|
-
return find(choices, (o) => o.value === value) || null;
|
|
62
|
-
} else {
|
|
63
|
-
return null;
|
|
109
|
+
if (value === undefined || !value || value.length === 0) return null;
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
// a list of values, like ['foo', 'bar'];
|
|
113
|
+
return value.map((v) => normalizeValue(choices, v));
|
|
64
114
|
}
|
|
115
|
+
|
|
116
|
+
if (isObject(value)) {
|
|
117
|
+
// an object like `{label, value}` or `{ title, value }`
|
|
118
|
+
return normalizeSingleSelectOption(value, intl);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// fallback: treat value as a token and look it up in choices
|
|
122
|
+
return Object.keys(choiceMap).includes(value)
|
|
123
|
+
? { label: choiceMap[value], value }
|
|
124
|
+
: { label: value, value };
|
|
65
125
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { normalizeValue } from './SelectUtils';
|
|
1
|
+
import { normalizeValue, convertValueToVocabQuery } from './SelectUtils';
|
|
2
2
|
|
|
3
3
|
describe('normalizeValue', () => {
|
|
4
4
|
it('Given an object/object, p.restapi title/token', () => {
|
|
@@ -98,4 +98,79 @@ describe('normalizeValue', () => {
|
|
|
98
98
|
const value = undefined;
|
|
99
99
|
expect(normalizeValue(choices, value)).toStrictEqual(null);
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
it('Given an array of tokenized value objects, with no choices', () => {
|
|
103
|
+
const choices = [];
|
|
104
|
+
const value = [
|
|
105
|
+
{ title: 'Option 1', value: 'opt1' },
|
|
106
|
+
{ title: 'Option 2', value: 'opt2' },
|
|
107
|
+
];
|
|
108
|
+
expect(normalizeValue(choices, value)).toStrictEqual([
|
|
109
|
+
{ label: 'Option 1', value: 'opt1' },
|
|
110
|
+
{ label: 'Option 2', value: 'opt2' },
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('Given an array of strings, with no choices', () => {
|
|
115
|
+
const choices = [];
|
|
116
|
+
const value = ['opt1', 'opt2'];
|
|
117
|
+
expect(normalizeValue(choices, value)).toStrictEqual([
|
|
118
|
+
{ label: 'opt1', value: 'opt1' },
|
|
119
|
+
{ label: 'opt2', value: 'opt2' },
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('convertValueToVocabQuery', () => {
|
|
125
|
+
it('converts an array of token/title to token query', () => {
|
|
126
|
+
const value = [
|
|
127
|
+
{
|
|
128
|
+
title: 'Option 1',
|
|
129
|
+
token: 'option1',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
title: 'Option 100',
|
|
133
|
+
token: 'option100',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
title: 'Option 103',
|
|
137
|
+
token: 'option103',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
expect(convertValueToVocabQuery(value)).toStrictEqual({
|
|
141
|
+
tokens: ['option1', 'option100', 'option103'],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('converts an array of label/value to tokens query', () => {
|
|
146
|
+
const value = [
|
|
147
|
+
{
|
|
148
|
+
label: 'Option 1',
|
|
149
|
+
value: 'option1',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: 'Option 100',
|
|
153
|
+
value: 'option100',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
label: 'Option 103',
|
|
157
|
+
value: 'option103',
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
expect(convertValueToVocabQuery(value)).toStrictEqual({
|
|
161
|
+
tokens: ['option1', 'option100', 'option103'],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('converts arrays of strings to tokens query', () => {
|
|
166
|
+
const value = ['option1', 'option100', 'option103'];
|
|
167
|
+
expect(convertValueToVocabQuery(value)).toStrictEqual({
|
|
168
|
+
tokens: ['option1', 'option100', 'option103'],
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('converts a string to token query', () => {
|
|
173
|
+
const value = 'option1';
|
|
174
|
+
expect(convertValueToVocabQuery(value)).toStrictEqual({ token: 'option1' });
|
|
175
|
+
});
|
|
101
176
|
});
|