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