@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.
Files changed (218) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +24 -3
  3. package/locales/ca/LC_MESSAGES/volto.po +12 -2
  4. package/locales/ca.json +1 -1
  5. package/locales/de/LC_MESSAGES/volto.po +12 -2
  6. package/locales/de.json +1 -1
  7. package/locales/en/LC_MESSAGES/volto.po +12 -2
  8. package/locales/en.json +1 -1
  9. package/locales/es/LC_MESSAGES/volto.po +12 -2
  10. package/locales/es.json +1 -1
  11. package/locales/eu/LC_MESSAGES/volto.po +12 -2
  12. package/locales/eu.json +1 -1
  13. package/locales/fr/LC_MESSAGES/volto.po +12 -2
  14. package/locales/fr.json +1 -1
  15. package/locales/it/LC_MESSAGES/volto.po +12 -2
  16. package/locales/it.json +1 -1
  17. package/locales/ja/LC_MESSAGES/volto.po +12 -2
  18. package/locales/ja.json +1 -1
  19. package/locales/nl/LC_MESSAGES/volto.po +12 -2
  20. package/locales/nl.json +1 -1
  21. package/locales/pt/LC_MESSAGES/volto.po +12 -2
  22. package/locales/pt.json +1 -1
  23. package/locales/pt_BR/LC_MESSAGES/volto.po +12 -2
  24. package/locales/pt_BR.json +1 -1
  25. package/locales/ro/LC_MESSAGES/volto.po +12 -2
  26. package/locales/ro.json +1 -1
  27. package/locales/volto.pot +12 -2
  28. package/package.json +3 -2
  29. package/src/actions/vocabularies/vocabularies.js +15 -3
  30. package/src/components/index.js +1 -0
  31. package/src/components/manage/Add/Add.jsx +1 -0
  32. package/src/components/manage/Blocks/HeroImageLeft/Edit.jsx +1 -1
  33. package/src/components/manage/Blocks/Listing/getAsyncData.js +1 -1
  34. package/src/components/manage/Blocks/Text/Edit.jsx +19 -0
  35. package/src/components/manage/Edit/Edit.jsx +1 -0
  36. package/src/components/manage/Form/Form.jsx +32 -6
  37. package/src/components/manage/Form/UndoToolbar.jsx +78 -0
  38. package/src/components/manage/Multilingual/TranslationObject.jsx +1 -0
  39. package/src/components/manage/Widgets/AlignWidget.stories.jsx +5 -21
  40. package/src/components/manage/Widgets/ArrayWidget.jsx +88 -88
  41. package/src/components/manage/Widgets/ArrayWidget.stories.jsx +37 -64
  42. package/src/components/manage/Widgets/CheckboxWidget.stories.jsx +5 -22
  43. package/src/components/manage/Widgets/DatetimeWidget.jsx +65 -74
  44. package/src/components/manage/Widgets/DatetimeWidget.stories.jsx +7 -23
  45. package/src/components/manage/Widgets/DatetimeWidget.test.jsx +17 -15
  46. package/src/components/manage/Widgets/EmailWidget.stories.jsx +5 -22
  47. package/src/components/manage/Widgets/FileWidget.stories.jsx +5 -22
  48. package/src/components/manage/Widgets/NumberWidget.stories.jsx +5 -23
  49. package/src/components/manage/Widgets/ObjectBrowserWidget.stories.js +24 -32
  50. package/src/components/manage/Widgets/ObjectListWidget.stories.js +44 -44
  51. package/src/components/manage/Widgets/ObjectWidget.stories.jsx +13 -28
  52. package/src/components/manage/Widgets/PasswordWidget.stories.jsx +5 -22
  53. package/src/components/manage/Widgets/QueryWidget.jsx +2 -2
  54. package/src/components/manage/Widgets/QueryWidget.stories.jsx +1637 -22
  55. package/src/components/manage/Widgets/SelectAutoComplete.jsx +79 -48
  56. package/src/components/manage/Widgets/SelectAutoComplete.test.jsx +16 -0
  57. package/src/components/manage/Widgets/SelectAutocompleteWidget.stories.jsx +161 -0
  58. package/src/components/manage/Widgets/SelectUtils.js +90 -30
  59. package/src/components/manage/Widgets/SelectUtils.test.jsx +76 -1
  60. package/src/components/manage/Widgets/SelectWidget.jsx +26 -37
  61. package/src/components/manage/Widgets/SelectWidget.stories.jsx +96 -28
  62. package/src/components/manage/Widgets/TextWidget.stories.jsx +5 -22
  63. package/src/components/manage/Widgets/TextareaWidget.stories.jsx +5 -22
  64. package/src/components/manage/Widgets/TokenWidget.jsx +19 -17
  65. package/src/components/manage/Widgets/TokenWidget.stories.jsx +141 -0
  66. package/src/components/manage/Widgets/UrlWidget.stories.jsx +5 -21
  67. package/src/components/manage/Widgets/VocabularyTermsWidget.stories.js +27 -64
  68. package/src/components/manage/Widgets/WysiwygWidget.stories.jsx +5 -22
  69. package/src/components/manage/Widgets/story.jsx +38 -0
  70. package/src/components/theme/ContactForm/ContactForm.jsx +1 -0
  71. package/src/components/theme/ContactForm/ContactForm.stories.jsx +126 -0
  72. package/src/components/theme/CorsError/CorsError.jsx +2 -2
  73. package/src/components/theme/Navigation/NavItem.jsx +3 -1
  74. package/src/config/Blocks.jsx +8 -1
  75. package/src/config/Loadables.jsx +2 -0
  76. package/src/config/index.js +3 -0
  77. package/src/helpers/UndoManager/useUndoManager.js +102 -0
  78. package/src/helpers/index.js +1 -0
  79. package/src/reducers/vocabularies/vocabularies.js +13 -2
  80. package/src/store.js +1 -1
  81. package/src/storybook.jsx +55 -0
  82. package/theme/themes/pastanaga/extras/time-picker-overrides.less +1 -1
  83. package/include/python3.8/Python-ast.h +0 -715
  84. package/include/python3.8/Python.h +0 -160
  85. package/include/python3.8/abstract.h +0 -844
  86. package/include/python3.8/asdl.h +0 -46
  87. package/include/python3.8/ast.h +0 -37
  88. package/include/python3.8/bitset.h +0 -23
  89. package/include/python3.8/bltinmodule.h +0 -14
  90. package/include/python3.8/boolobject.h +0 -34
  91. package/include/python3.8/bytearrayobject.h +0 -62
  92. package/include/python3.8/bytes_methods.h +0 -69
  93. package/include/python3.8/bytesobject.h +0 -224
  94. package/include/python3.8/cellobject.h +0 -29
  95. package/include/python3.8/ceval.h +0 -231
  96. package/include/python3.8/classobject.h +0 -59
  97. package/include/python3.8/code.h +0 -180
  98. package/include/python3.8/codecs.h +0 -240
  99. package/include/python3.8/compile.h +0 -106
  100. package/include/python3.8/complexobject.h +0 -69
  101. package/include/python3.8/context.h +0 -84
  102. package/include/python3.8/cpython/abstract.h +0 -319
  103. package/include/python3.8/cpython/dictobject.h +0 -94
  104. package/include/python3.8/cpython/fileobject.h +0 -24
  105. package/include/python3.8/cpython/initconfig.h +0 -434
  106. package/include/python3.8/cpython/interpreteridobject.h +0 -19
  107. package/include/python3.8/cpython/object.h +0 -470
  108. package/include/python3.8/cpython/objimpl.h +0 -113
  109. package/include/python3.8/cpython/pyerrors.h +0 -188
  110. package/include/python3.8/cpython/pylifecycle.h +0 -78
  111. package/include/python3.8/cpython/pymem.h +0 -108
  112. package/include/python3.8/cpython/pystate.h +0 -252
  113. package/include/python3.8/cpython/sysmodule.h +0 -21
  114. package/include/python3.8/cpython/traceback.h +0 -22
  115. package/include/python3.8/cpython/tupleobject.h +0 -36
  116. package/include/python3.8/cpython/unicodeobject.h +0 -1239
  117. package/include/python3.8/datetime.h +0 -259
  118. package/include/python3.8/descrobject.h +0 -108
  119. package/include/python3.8/dictobject.h +0 -94
  120. package/include/python3.8/dtoa.h +0 -19
  121. package/include/python3.8/dynamic_annotations.h +0 -499
  122. package/include/python3.8/enumobject.h +0 -17
  123. package/include/python3.8/errcode.h +0 -38
  124. package/include/python3.8/eval.h +0 -37
  125. package/include/python3.8/fileobject.h +0 -49
  126. package/include/python3.8/fileutils.h +0 -185
  127. package/include/python3.8/floatobject.h +0 -130
  128. package/include/python3.8/frameobject.h +0 -92
  129. package/include/python3.8/funcobject.h +0 -104
  130. package/include/python3.8/genobject.h +0 -109
  131. package/include/python3.8/graminit.h +0 -94
  132. package/include/python3.8/grammar.h +0 -77
  133. package/include/python3.8/import.h +0 -149
  134. package/include/python3.8/internal/pycore_accu.h +0 -39
  135. package/include/python3.8/internal/pycore_atomic.h +0 -558
  136. package/include/python3.8/internal/pycore_ceval.h +0 -37
  137. package/include/python3.8/internal/pycore_code.h +0 -27
  138. package/include/python3.8/internal/pycore_condvar.h +0 -95
  139. package/include/python3.8/internal/pycore_context.h +0 -42
  140. package/include/python3.8/internal/pycore_fileutils.h +0 -54
  141. package/include/python3.8/internal/pycore_getopt.h +0 -22
  142. package/include/python3.8/internal/pycore_gil.h +0 -50
  143. package/include/python3.8/internal/pycore_hamt.h +0 -116
  144. package/include/python3.8/internal/pycore_initconfig.h +0 -166
  145. package/include/python3.8/internal/pycore_object.h +0 -81
  146. package/include/python3.8/internal/pycore_pathconfig.h +0 -75
  147. package/include/python3.8/internal/pycore_pyerrors.h +0 -62
  148. package/include/python3.8/internal/pycore_pyhash.h +0 -10
  149. package/include/python3.8/internal/pycore_pylifecycle.h +0 -118
  150. package/include/python3.8/internal/pycore_pymem.h +0 -212
  151. package/include/python3.8/internal/pycore_pystate.h +0 -326
  152. package/include/python3.8/internal/pycore_traceback.h +0 -96
  153. package/include/python3.8/internal/pycore_tupleobject.h +0 -19
  154. package/include/python3.8/internal/pycore_warnings.h +0 -25
  155. package/include/python3.8/interpreteridobject.h +0 -17
  156. package/include/python3.8/intrcheck.h +0 -33
  157. package/include/python3.8/iterobject.h +0 -25
  158. package/include/python3.8/listobject.h +0 -81
  159. package/include/python3.8/longintrepr.h +0 -99
  160. package/include/python3.8/longobject.h +0 -242
  161. package/include/python3.8/marshal.h +0 -28
  162. package/include/python3.8/memoryobject.h +0 -72
  163. package/include/python3.8/methodobject.h +0 -131
  164. package/include/python3.8/modsupport.h +0 -248
  165. package/include/python3.8/moduleobject.h +0 -90
  166. package/include/python3.8/namespaceobject.h +0 -19
  167. package/include/python3.8/node.h +0 -48
  168. package/include/python3.8/object.h +0 -753
  169. package/include/python3.8/objimpl.h +0 -284
  170. package/include/python3.8/odictobject.h +0 -43
  171. package/include/python3.8/opcode.h +0 -148
  172. package/include/python3.8/osdefs.h +0 -51
  173. package/include/python3.8/osmodule.h +0 -17
  174. package/include/python3.8/parsetok.h +0 -110
  175. package/include/python3.8/patchlevel.h +0 -35
  176. package/include/python3.8/picklebufobject.h +0 -31
  177. package/include/python3.8/py_curses.h +0 -100
  178. package/include/python3.8/pyarena.h +0 -64
  179. package/include/python3.8/pycapsule.h +0 -59
  180. package/include/python3.8/pyconfig.h +0 -1665
  181. package/include/python3.8/pyctype.h +0 -39
  182. package/include/python3.8/pydebug.h +0 -40
  183. package/include/python3.8/pydtrace.h +0 -59
  184. package/include/python3.8/pydtrace_probes.h +0 -228
  185. package/include/python3.8/pyerrors.h +0 -335
  186. package/include/python3.8/pyexpat.h +0 -55
  187. package/include/python3.8/pyfpe.h +0 -12
  188. package/include/python3.8/pyhash.h +0 -145
  189. package/include/python3.8/pylifecycle.h +0 -75
  190. package/include/python3.8/pymacconfig.h +0 -102
  191. package/include/python3.8/pymacro.h +0 -106
  192. package/include/python3.8/pymath.h +0 -230
  193. package/include/python3.8/pymem.h +0 -150
  194. package/include/python3.8/pyport.h +0 -850
  195. package/include/python3.8/pystate.h +0 -136
  196. package/include/python3.8/pystrcmp.h +0 -23
  197. package/include/python3.8/pystrhex.h +0 -22
  198. package/include/python3.8/pystrtod.h +0 -45
  199. package/include/python3.8/pythonrun.h +0 -210
  200. package/include/python3.8/pythread.h +0 -161
  201. package/include/python3.8/pytime.h +0 -246
  202. package/include/python3.8/rangeobject.h +0 -27
  203. package/include/python3.8/setobject.h +0 -108
  204. package/include/python3.8/sliceobject.h +0 -65
  205. package/include/python3.8/structmember.h +0 -74
  206. package/include/python3.8/structseq.h +0 -49
  207. package/include/python3.8/symtable.h +0 -123
  208. package/include/python3.8/sysmodule.h +0 -41
  209. package/include/python3.8/token.h +0 -92
  210. package/include/python3.8/traceback.h +0 -28
  211. package/include/python3.8/tracemalloc.h +0 -38
  212. package/include/python3.8/tupleobject.h +0 -48
  213. package/include/python3.8/typeslots.h +0 -85
  214. package/include/python3.8/ucnhash.h +0 -36
  215. package/include/python3.8/unicodeobject.h +0 -1044
  216. package/include/python3.8/warnings.h +0 -67
  217. package/include/python3.8/weakrefobject.h +0 -86
  218. package/src/components/theme/ContactForm/ContactForm.stories.mdx +0 -39
@@ -1,25 +1,30 @@
1
1
  /**
2
- * ArrayWidget component.
3
- * @module components/manage/Widgets/ArrayWidget
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
- * ArrayWidget component class.
48
- * @class ArrayWidget
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
- resolve(
150
- await this.props
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 { selectedOption } = this.state;
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?.[props.intl.locale];
234
-
235
- // If the schema already has the choices in it, then do not try to get the vocab,
236
- // even if there is one
237
- if (props.items?.choices) {
238
- return {
239
- choices: props.items.choices,
240
- };
241
- } else if (vocabState) {
242
- return {
243
- choices: vocabState.items,
244
- vocabBaseUrl,
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 { find, isBoolean, isObject, isArray } from 'lodash';
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 = find(choices, (o) => getBoolean(o[0]) === value);
94
+ const label = choiceMap[getBoolean(value)];
26
95
  return label
27
96
  ? {
28
- label: label[1],
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: this.props.intl.formatMessage(messages.no_value),
104
+ label: intl.formatMessage(messages.no_value),
38
105
  value: 'no-value',
39
106
  };
40
107
  }
41
108
 
42
- if (isArray(value) && choices.length > 0) {
43
- return value.map((v) => ({
44
- label: find(choices, (o) => o[0] === v)?.[1] || v,
45
- value: v,
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
  });