@plone/volto 14.0.0-alpha.17 → 14.0.0-alpha.20

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Change Log
2
2
 
3
+ ## 14.0.0-alpha.20 (2021-10-15)
4
+
5
+ ### Breaking
6
+
7
+ - Revisited, rethought and refactored Seamless mode Seamless mode @sneridagh
8
+ For more information, please read the deploying guide
9
+ https://docs.voltocms.com/deploying/seamless-mode/
10
+
11
+ and the upgrade guide
12
+ https://docs.voltocms.com/upgrade-guide/
13
+
14
+ ### Bugfix
15
+
16
+ - Fixed SelectWidget: when there was a selected value, the selection was lost when the tab was changed. @giuliaghisini
17
+
18
+ ## 14.0.0-alpha.19 (2021-10-15)
19
+
20
+ ### Feature
21
+
22
+ - Make VocabularyTermsWidget orderable @ksuess
23
+ - Get widget by tagged values @ksuess
24
+
25
+ ## 14.0.0-alpha.18 (2021-10-11)
26
+
27
+ ### Internal
28
+
29
+ - Re-release last release, since it does not show on NPM @sneridagh
30
+
3
31
  ## 14.0.0-alpha.17 (2021-10-11)
4
32
 
5
33
  ### Breaking
@@ -8,7 +36,7 @@
8
36
 
9
37
  ### Bugfix
10
38
 
11
- -Add spinner on sharing View Button @iRohitSingh
39
+ - Add spinner on sharing View Button @iRohitSingh
12
40
 
13
41
  ## 14.0.0-alpha.16 (2021-10-10)
14
42
 
@@ -24,7 +52,7 @@
24
52
 
25
53
  - Adjusted main `Logo` component styling @sneridagh
26
54
 
27
- For a more information, please read the upgrade guide
55
+ For more information, please read the upgrade guide
28
56
  https://docs.voltocms.com/upgrade-guide/
29
57
 
30
58
  ### Feature
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "14.0.0-alpha.17",
12
+ "version": "14.0.0-alpha.20",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -73,7 +73,7 @@
73
73
  "ci:start-frontend-multilingual": "RAZZLE_TESTING_ADDONS=core-sandbox:multilingualFixture RAZZLE_API_PATH=http://localhost:55001/plone yarn build && start-test start:prod http://localhost:3000 cypress:run:multilingual",
74
74
  "ci:start-frontend-workingCopy": "RAZZLE_TESTING_ADDONS=core-sandbox:workingCopyFixture RAZZLE_API_PATH=http://localhost:55001/plone yarn build && start-test start:prod http://localhost:3000 cypress:run:workingCopy",
75
75
  "ci:start-project-frontend": "cd my-volto-app && RAZZLE_API_PATH=http://localhost:55001/plone yarn build && start-test start:prod http://localhost:3000 'cd .. && yarn cypress:run'",
76
- "ci:start-frontend-guillotina": "RAZZLE_TESTING_ADDONS=volto-guillotina RAZZLE_API_PATH=http://localhost:8081/db/web yarn build && start-test start:prod http://localhost:3000 cypress:run:guillotina",
76
+ "ci:start-frontend-guillotina": "RAZZLE_TESTING_ADDONS=volto-guillotina RAZZLE_API_PATH=http://localhost:8081/db/web RAZZLE_LEGACY_TRAVERSE=true yarn build && start-test start:prod http://localhost:3000 cypress:run:guillotina",
77
77
  "ci:cypress:run": "start-test ci:start-api-plone http-get://localhost:55001/plone ci:start-frontend",
78
78
  "ci:cypress:project:run": "start-test ci:start-api-plone http-get://localhost:55001/plone ci:start-project-frontend",
79
79
  "ci:cypress:run:core-sandbox": "start-test ci:start-api-plone http-get://localhost:55001/plone ci:start-frontend-core-sandbox",
@@ -45,6 +45,25 @@ const getWidgetByName = (widget) =>
45
45
  ? config.widgets.widget[widget] || getWidgetDefault()
46
46
  : null;
47
47
 
48
+ /**
49
+ * Get widget by tagged values
50
+ * @param {object} widgetOptions
51
+ * @returns {string} Widget component.
52
+ *
53
+
54
+ directives.widget(
55
+ 'fieldname',
56
+ frontendOptions={
57
+ "widget": 'specialwidget',
58
+ "version": 'extra'
59
+ })
60
+
61
+ */
62
+ const getWidgetFromTaggedValues = (widgetOptions) =>
63
+ typeof widgetOptions?.frontendOptions?.widget === 'string'
64
+ ? config.widgets.widget[widgetOptions.frontendOptions.widget]
65
+ : null;
66
+
48
67
  /**
49
68
  * Get widget by field's `vocabulary` attribute
50
69
  * @method getWidgetByVocabulary
@@ -115,6 +134,7 @@ const getWidgetByType = (type) => config.widgets.type[type] || null;
115
134
  const Field = (props, { intl }) => {
116
135
  const Widget =
117
136
  getWidgetByFieldId(props.id) ||
137
+ getWidgetFromTaggedValues(props.widgetOptions) ||
118
138
  getWidgetByName(props.widget) ||
119
139
  getWidgetByChoices(props) ||
120
140
  getWidgetByVocabulary(props.vocabulary) ||
@@ -222,4 +222,27 @@ describe('Field', () => {
222
222
  const json = component.toJSON();
223
223
  expect(json).toMatchSnapshot();
224
224
  });
225
+
226
+ it('renders a widget regarding the priority of tagged value over name', () => {
227
+ const store = mockStore({
228
+ intl: {
229
+ locale: 'en',
230
+ messages: {},
231
+ },
232
+ });
233
+ const component = renderer.create(
234
+ <Provider store={store}>
235
+ <>
236
+ <Field widget="textarea" id="test" />
237
+ <Field
238
+ widget="textarea"
239
+ widgetOptions={{ frontendOptions: { widget: 'richtext' } }}
240
+ id="test"
241
+ />
242
+ </>
243
+ </Provider>,
244
+ );
245
+ const json = component.toJSON();
246
+ expect(json).toMatchSnapshot();
247
+ });
225
248
  });
@@ -155,11 +155,26 @@ class SelectWidget extends Component {
155
155
  * @returns {undefined}
156
156
  */
157
157
  componentDidMount() {
158
- if (!this.props.choices?.length && this.props.vocabBaseUrl) {
158
+ if (
159
+ (!this.props.choices || this.props.choices?.length === 0) &&
160
+ this.props.vocabBaseUrl
161
+ ) {
159
162
  this.props.getVocabulary(this.props.vocabBaseUrl);
160
163
  }
161
164
  }
162
165
 
166
+ componentDidUpdate() {
167
+ if (
168
+ !this.state.selectedOption &&
169
+ this.props.value &&
170
+ this.props.choices?.length > 0
171
+ ) {
172
+ this.setState({
173
+ selectedOption: normalizeValue(this.props.choices, this.props.value),
174
+ });
175
+ }
176
+ }
177
+
163
178
  /**
164
179
  * Initiate search with new query
165
180
  * @method loadOptions
@@ -1,41 +1,106 @@
1
1
  /**
2
2
  * VocabularyTermsWidget
3
3
  * @module components/manage/Widgets/VocabularyTermsWidget
4
- * Widget for dict field
5
- * - with value_type TextLine field
6
- * - or value_type Dict field for translations
7
- *
8
- * values are editable
9
- * keys are generated
10
- * Purpose: Use this widget for a dict field (of controlpanel),
11
- * that acts as a source of a vocabulary for a Choice field.
12
- * Vocabulary terms should change over time only in title (corresponding dictionary value), not value (corresponding dictionary key),
4
+ * Widget for plone.schema.JSONField field meant for a SimpleVocabulary source
5
+ *
6
+
7
+ VOCABULARY_SCHEMA = json.dumps(
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "items": {
12
+ "type": "array",
13
+ "items": {
14
+ "type": "object",
15
+ "properties": {
16
+ "token": {"type": "string"},
17
+ "titles": {
18
+ "type": "object",
19
+ "properties": {
20
+ "lang": {"type": "string"},
21
+ "title": {"type": "string"},
22
+ }
23
+ },
24
+ }
25
+ }
26
+ }
27
+ },
28
+ }
29
+ )
30
+
31
+
32
+ class IPloneconfSettings(Interface):
33
+
34
+ types_of_foo = schema.JSONField(
35
+ title="Types of Foo",
36
+ description="Available types of a foo",
37
+ required=False,
38
+ schema=VOCABULARY_SCHEMA,
39
+ widget="vocabularyterms",
40
+ default={"items": [
41
+ {
42
+ "token": "talk",
43
+ "titles": {
44
+ "en": "Talk",
45
+ "de": "Vortrag",
46
+ }
47
+ },
48
+ {
49
+ "token": "lightning-talk",
50
+ "titles": {
51
+ "en": "Lightning-Talk",
52
+ "de": "kürzerer erleuchtender Vortrag",
53
+ }
54
+ },
55
+ ]},
56
+ missing_value={"items": []},
57
+ )
58
+
59
+
60
+ @provider(IVocabularyFactory)
61
+ def TalkTypesVocabulary(context):
62
+ name = "ploneconf.types_of_talk"
63
+ registry_record_value = api.portal.get_registry_record(name)
64
+ items = registry_record_value.get('items', [])
65
+ lang = api.portal.get_current_language()
66
+ return SimpleVocabulary.fromItems([[item['token'], item['token'], item['titles'][lang]] for item in items])
67
+
68
+
69
+ * titles are editable
70
+ * tokens are generated
71
+ *
72
+ * Purpose: Use this widget for a controlpanel field
73
+ * that acts as a source of a vocabulary for a zope.schema.Choice field.
74
+ * Vocabulary terms should change over time only in title, not value,
13
75
  * as vocabulary term values are stored on content type instances.
14
76
  *
15
- * The widget has two versions depending if you apply it (widget='vocabularyterms') to a plone.schema Dict field
16
- * - with value_type TextLine field
17
- * - or value_type Dict field for translations
18
- * The latter provides fields for all config.settings.supportedLanguages
77
+ * Apply widget with `widget='vocabularyterms'`
78
+ * Future widget directive coming: Apply widget with directive widget
19
79
  *
20
- * See storybook for a demo.
80
+ * See storybook for a demo: Run
81
+ * `yarn storybook`
82
+ * or see https://docs.voltocms.com/storybook/
21
83
  */
22
84
 
23
85
  import React from 'react';
24
86
  import { useDispatch } from 'react-redux';
25
- import { keys, values } from 'lodash';
87
+ import { findIndex, remove } from 'lodash';
26
88
  import { defineMessages, useIntl } from 'react-intl';
27
89
  import { v4 as uuid } from 'uuid';
28
90
 
29
- import { Button, Divider, Grid, Input, Segment } from 'semantic-ui-react';
91
+ import { Button, Segment } from 'semantic-ui-react';
30
92
 
31
- import { FormFieldWrapper, Icon, ObjectWidget } from '@plone/volto/components';
93
+ import {
94
+ DragDropList,
95
+ FormFieldWrapper,
96
+ Icon,
97
+ ObjectWidget,
98
+ } from '@plone/volto/components';
32
99
  import { langmap } from '@plone/volto/helpers';
33
100
 
34
101
  import deleteSVG from '@plone/volto/icons/delete.svg';
35
102
  import addSVG from '@plone/volto/icons/add.svg';
36
- import clearSVG from '@plone/volto/icons/clear.svg';
37
-
38
- import config from '@plone/volto/registry';
103
+ import dragSVG from '@plone/volto/icons/drag.svg';
39
104
 
40
105
  const messages = defineMessages({
41
106
  title: {
@@ -66,26 +131,36 @@ const messages = defineMessages({
66
131
 
67
132
  const VocabularyTermsWidget = (props) => {
68
133
  const { id, value = {}, onChange } = props;
134
+ var widgetvalue = value;
69
135
  const dispatch = useDispatch();
70
136
  const [toFocusId, setToFocusId] = React.useState('');
71
137
  const intl = useIntl();
72
138
 
73
- // sort terms by values
74
- // sort terms with translations by defaultLanguage if provided by config
75
- const defaultLanguage =
76
- config.settings.defaultLanguage ?? config.settings.supportedLanguages[0];
77
- const vocabularytokens =
78
- props.value_type?.schema?.type === 'string'
79
- ? keys(value).sort((a, b) => {
80
- return value[a].localeCompare(value[b]);
81
- })
82
- : keys(value).sort((a, b) => {
83
- return defaultLanguage
84
- ? value[a][defaultLanguage]?.localeCompare(
85
- value[b][defaultLanguage],
86
- )
87
- : values(value[a])[0].localeCompare(values(value[b])[0]);
88
- });
139
+ React.useEffect(() => {
140
+ const element = document.getElementById(toFocusId);
141
+ element && element.focus();
142
+ setToFocusId('');
143
+ }, [dispatch, toFocusId]);
144
+
145
+ // LEGACY: value from unordered zope.schema.Dict instead of zope.schema.JSONField
146
+ if (widgetvalue.items === undefined) {
147
+ widgetvalue = {
148
+ items: Object.keys(widgetvalue).map((key) => {
149
+ return {
150
+ token: key,
151
+ titles: {
152
+ en: widgetvalue[key],
153
+ },
154
+ };
155
+ }),
156
+ };
157
+ }
158
+
159
+ let vocabularyterms = widgetvalue.items;
160
+
161
+ let supportedLanguages = Object.keys(
162
+ vocabularyterms?.map((el) => el.titles)?.pop() || {},
163
+ );
89
164
 
90
165
  const TermSchema = {
91
166
  title: 'Translation of term',
@@ -93,11 +168,11 @@ const VocabularyTermsWidget = (props) => {
93
168
  {
94
169
  id: 'default',
95
170
  title: 'Email',
96
- fields: config.settings.supportedLanguages,
171
+ fields: supportedLanguages,
97
172
  },
98
173
  ],
99
174
  properties: Object.fromEntries(
100
- config.settings.supportedLanguages.map((languageIdentifier) => [
175
+ supportedLanguages.map((languageIdentifier) => [
101
176
  languageIdentifier,
102
177
  {
103
178
  title: langmap[languageIdentifier]?.nativeName ?? languageIdentifier,
@@ -107,37 +182,40 @@ const VocabularyTermsWidget = (props) => {
107
182
  required: [],
108
183
  };
109
184
 
110
- React.useEffect(() => {
111
- const element = document.getElementById(toFocusId);
112
- element && element.focus();
113
- }, [dispatch, toFocusId, value]);
114
-
115
- function onChangeFieldHandler(dictkey, fieldvalue) {
116
- onChange(id, { ...value, [dictkey]: fieldvalue });
117
- if (typeof fieldvalue === 'string') {
118
- setToFocusId(props.id + '-' + dictkey);
119
- }
185
+ function onChangeFieldHandler(token, fieldid, fieldvalue) {
186
+ let index = findIndex(widgetvalue.items, { token: token });
187
+ let newitems = widgetvalue.items;
188
+ newitems.splice(index, 1, {
189
+ token: token,
190
+ titles: fieldvalue,
191
+ });
192
+ onChange(id, {
193
+ items: newitems,
194
+ });
120
195
  }
121
196
 
122
197
  function addTermHandler(e) {
123
198
  e.preventDefault();
124
- const newdictkey = uuid();
125
- if (props.value_type?.schema?.type === 'string') {
126
- onChange(id, {
127
- ...value,
128
- [newdictkey]: '',
129
- });
130
- setToFocusId(props.id + '-' + newdictkey);
131
- } else {
132
- onChange(id, {
133
- ...value,
134
- [newdictkey]: Object.fromEntries(
135
- config.settings.supportedLanguages.map((el) => [el, '']),
136
- ),
137
- });
138
- }
199
+ const newtoken = uuid();
200
+ let newitems = widgetvalue.items;
201
+ newitems.push({
202
+ token: newtoken,
203
+ titles: Object.fromEntries(supportedLanguages.map((el) => [el, ''])),
204
+ });
205
+ onChange(id, {
206
+ items: newitems,
207
+ });
208
+ setToFocusId(`field-${supportedLanguages[0]}-0-${id}-${newtoken}`);
209
+ }
210
+
211
+ function swap(arr, from, to) {
212
+ arr.splice(from, 1, arr.splice(to, 1, arr[from])[0]);
139
213
  }
140
214
 
215
+ let enhancedvocabularyterms = vocabularyterms.map((el) => {
216
+ return { ...el, '@id': el.token };
217
+ });
218
+
141
219
  return (
142
220
  <FormFieldWrapper {...props} className="dictwidget">
143
221
  <Segment basic>
@@ -154,84 +232,93 @@ const VocabularyTermsWidget = (props) => {
154
232
  {intl.formatMessage(messages.addTerm)}
155
233
  </Button>
156
234
  </div>
157
- <Grid className="entries-list">
158
- {vocabularytokens.map((dictkey, index) => {
159
- return (
160
- <Grid.Row stretched className="entry-wrapper" key={index}>
161
- <Grid.Column width="1">
162
- <Button.Group>
163
- <Button
164
- basic
165
- className="cancel"
166
- title={intl.formatMessage(messages.removeTerm)}
167
- aria-label={`${intl.formatMessage(messages.removeTerm)} #${
168
- index + 1
169
- }`}
170
- onClick={(e) => {
171
- e.preventDefault();
172
- let dct = { ...value };
173
- delete dct[dictkey];
174
- onChange(id, dct);
175
- }}
176
- >
177
- <Icon name={deleteSVG} size="20px" color="#e40166" />
178
- </Button>
179
- </Button.Group>
180
- </Grid.Column>
181
- <Grid.Column width="10">
182
- {typeof value[dictkey] === 'string' ? (
183
- <Input
184
- id={`${props.id}-${dictkey}`}
185
- name={`${props.id}-${dictkey}`}
186
- type="text"
187
- placeholder={intl.formatMessage(messages.termtitlelabel)}
188
- required={true}
189
- value={value[dictkey]}
190
- onChange={(e, target) => {
191
- e.preventDefault();
192
- e.stopPropagation();
193
- onChangeFieldHandler(dictkey, target.value);
194
- }}
195
- />
196
- ) : (
197
- <>
198
- <ObjectWidget
199
- id={`${id}-${dictkey}`}
200
- onChange={(id, value) => {
201
- onChangeFieldHandler(dictkey, value);
202
- }}
203
- value={value[dictkey]}
204
- schema={TermSchema}
205
- title="Translation of term"
206
- />
207
- <Divider />
208
- </>
209
- )}
210
- </Grid.Column>
211
- <Grid.Column width="1">
212
- {value[dictkey]?.length > 0 && (
213
- <Button.Group>
214
- <Button
215
- basic
216
- className="cancel"
217
- title={intl.formatMessage(messages.clearTermTitle)}
218
- onClick={(e) => {
219
- e.preventDefault();
220
- e.stopPropagation();
221
- onChangeFieldHandler(dictkey, '');
222
- }}
223
- >
224
- <Icon name={clearSVG} size="20px" />
225
- </Button>
226
- </Button.Group>
227
- )}
228
- </Grid.Column>
229
- </Grid.Row>
235
+ <DragDropList
236
+ childList={enhancedvocabularyterms.map((o) => [o['@id'], o])}
237
+ onMoveItem={(result) => {
238
+ const { source, destination } = result;
239
+ if (!destination) {
240
+ return;
241
+ }
242
+ let newitems = widgetvalue.items;
243
+ swap(newitems, source.index, destination.index);
244
+ onChange(id, {
245
+ items: newitems,
246
+ });
247
+ return true;
248
+ }}
249
+ >
250
+ {(dragProps) => {
251
+ const { child, childId, index } = dragProps;
252
+ let termProps = {
253
+ index: index,
254
+ id,
255
+ vocabularyterms,
256
+ vterm: child,
257
+ onChange,
258
+ };
259
+ return termsWrapper(
260
+ dragProps,
261
+ <ObjectWidget
262
+ id={`${id}-${child.token}`}
263
+ key={childId}
264
+ onChange={(fieldid, fieldvalue) => {
265
+ onChangeFieldHandler(child.token, fieldid, fieldvalue);
266
+ }}
267
+ value={child.titles}
268
+ schema={TermSchema}
269
+ title="Translation of term"
270
+ />,
271
+ termProps,
230
272
  );
231
- })}
232
- </Grid>
273
+ }}
274
+ </DragDropList>
233
275
  </FormFieldWrapper>
234
276
  );
235
277
  };
236
278
 
279
+ const termsWrapper = ({ draginfo }, OW, termProps) => (
280
+ <TermsWrapper draginfo={draginfo} termProps={termProps}>
281
+ {OW}
282
+ </TermsWrapper>
283
+ );
284
+
285
+ const TermsWrapper = (props) => {
286
+ const intl = useIntl();
287
+ const { termProps, draginfo, children } = props;
288
+ const { id, vocabularyterms, vterm, onChange } = termProps;
289
+
290
+ return (
291
+ <div
292
+ ref={draginfo.innerRef}
293
+ {...draginfo.draggableProps}
294
+ className="vocabularyterm"
295
+ >
296
+ <div style={{ alignItems: 'center', display: 'flex' }}>
297
+ <div {...draginfo.dragHandleProps} className="draghandlewrapper">
298
+ <Icon name={dragSVG} size="18px" />
299
+ </div>
300
+ <div className="ui drag block inner">{children}</div>
301
+ <div>
302
+ <Button
303
+ icon
304
+ basic
305
+ className="delete-button"
306
+ title={intl.formatMessage(messages.removeTerm)}
307
+ aria-label={`${intl.formatMessage(messages.removeTerm)} #${
308
+ vterm.token
309
+ }`}
310
+ onClick={(e) => {
311
+ e.preventDefault();
312
+ remove(vocabularyterms, (el) => el.token === vterm.token);
313
+ onChange(id, { items: vocabularyterms });
314
+ }}
315
+ >
316
+ <Icon name={deleteSVG} size="20px" color="#e40166" />
317
+ </Button>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ );
322
+ };
323
+
237
324
  export default VocabularyTermsWidget;
@@ -10,18 +10,26 @@ const customStore = {
10
10
  },
11
11
  };
12
12
 
13
- const customStoreTranslations = {
14
- userSession: { token: '1234' },
15
- intl: {
16
- locale: 'it',
17
- messages: {},
18
- },
19
- };
20
-
21
- const WrappedSimple = (args) => {
13
+ const WrappedJSONField = (args) => {
22
14
  const [value, setValue] = React.useState({
23
- '001': 'manual',
24
- '002': 'questions & answers',
15
+ items: [
16
+ {
17
+ token: 'talk',
18
+ titles: {
19
+ en: 'Talk',
20
+ de: 'Vortrag',
21
+ it: 'Lettura',
22
+ },
23
+ },
24
+ {
25
+ token: 'lightning-talk',
26
+ titles: {
27
+ en: 'Lightning-Talk',
28
+ de: 'kürzerer erleuchtender Vortrag',
29
+ it: 'Lightning-Talk',
30
+ },
31
+ },
32
+ ],
25
33
  });
26
34
  const onChange = (block, value) => setValue(value);
27
35
 
@@ -33,15 +41,10 @@ const WrappedSimple = (args) => {
33
41
  <div className="ui segment form attached">
34
42
  <VocabularyTermsWidget
35
43
  {...args}
36
- id="Simple"
44
+ id="simplevocabulary"
37
45
  title="Vocabulary terms"
38
46
  block="testBlock"
39
47
  value={value}
40
- value_type={{
41
- schema: {
42
- type: 'string',
43
- },
44
- }}
45
48
  onChange={onChange}
46
49
  />
47
50
  <pre>{JSON.stringify(value, null, 4)}</pre>
@@ -50,33 +53,30 @@ const WrappedSimple = (args) => {
50
53
  );
51
54
  };
52
55
 
53
- const WrappedTranslations = (args) => {
56
+ const WrappedSimple = (args) => {
54
57
  const [value, setValue] = React.useState({
55
- '001': {
56
- en: 'manual',
57
- it: 'manuale',
58
- de: 'Anleitung',
59
- },
60
- '002': {
61
- en: 'questions & answers',
62
- it: 'domande frequenti',
63
- de: 'FAQs',
64
- },
58
+ '001': 'manual',
59
+ '002': 'questions & answers',
65
60
  });
66
61
  const onChange = (block, value) => setValue(value);
67
62
 
68
63
  return (
69
64
  <Wrapper
70
65
  location={{ pathname: '/folder2/folder21/doc212' }}
71
- customStore={customStoreTranslations}
66
+ customStore={customStore}
72
67
  >
73
68
  <div className="ui segment form attached">
74
69
  <VocabularyTermsWidget
75
70
  {...args}
76
- id="Translations"
71
+ id="Simple"
77
72
  title="Vocabulary terms"
78
73
  block="testBlock"
79
74
  value={value}
75
+ value_type={{
76
+ schema: {
77
+ type: 'string',
78
+ },
79
+ }}
80
80
  onChange={onChange}
81
81
  />
82
82
  <pre>{JSON.stringify(value, null, 4)}</pre>
@@ -97,6 +97,5 @@ export default {
97
97
  ],
98
98
  };
99
99
 
100
+ export const JSONField = () => <WrappedJSONField />;
100
101
  export const Simple = () => <WrappedSimple />;
101
-
102
- export const Translations = () => <WrappedTranslations />;
@@ -5,8 +5,15 @@ import { Provider } from 'react-intl-redux';
5
5
 
6
6
  import VocabularyTermsWidget from './VocabularyTermsWidget';
7
7
 
8
+ let mockSerial = 0;
8
9
  const mockStore = configureStore();
9
10
 
11
+ jest.mock('uuid', () => {
12
+ return {
13
+ v4: jest.fn().mockImplementation(() => `id-${mockSerial++}`),
14
+ };
15
+ });
16
+
10
17
  test('renders a dictionary widget component', () => {
11
18
  const store = mockStore({
12
19
  intl: {
@@ -14,6 +21,34 @@ test('renders a dictionary widget component', () => {
14
21
  messages: {},
15
22
  },
16
23
  });
24
+ let initialValue = {
25
+ items: [
26
+ {
27
+ token: 'talk',
28
+ titles: {
29
+ en: 'Talk',
30
+ de: 'Vortrag',
31
+ it: 'Lettura',
32
+ },
33
+ },
34
+ {
35
+ token: 'keynote',
36
+ titles: {
37
+ en: 'Keynote',
38
+ de: 'Keynote',
39
+ it: 'Keynote',
40
+ },
41
+ },
42
+ {
43
+ token: 'lightning-talk',
44
+ titles: {
45
+ en: 'Lightning-Talk',
46
+ de: 'kürzerer erleuchtender Vortrag',
47
+ it: 'Lightning-Talk',
48
+ },
49
+ },
50
+ ],
51
+ };
17
52
  const component = renderer.create(
18
53
  <Provider store={store}>
19
54
  <VocabularyTermsWidget
@@ -23,10 +58,7 @@ test('renders a dictionary widget component', () => {
23
58
  onChange={() => {}}
24
59
  onBlur={() => {}}
25
60
  onClick={() => {}}
26
- value={{
27
- manual: 'Manual',
28
- ubersicht: 'Übersicht',
29
- }}
61
+ value={initialValue}
30
62
  />
31
63
  </Provider>,
32
64
  );
@@ -92,6 +92,8 @@ let config = {
92
92
  actions_raising_api_errors: ['GET_CONTENT', 'UPDATE_CONTENT'],
93
93
  internalApiPath: process.env.RAZZLE_INTERNAL_API_PATH || undefined,
94
94
  websockets: process.env.RAZZLE_WEBSOCKETS || false,
95
+ // TODO: legacyTraverse to be removed when the use of the legacy traverse is deprecated.
96
+ legacyTraverse: process.env.RAZZLE_LEGACY_TRAVERSE || false,
95
97
  nonContentRoutes,
96
98
  extendedBlockRenderMap,
97
99
  blockStyleFn,
@@ -14,7 +14,7 @@ const filter = function (pathname, req) {
14
14
  return (
15
15
  __DEVELOPMENT__ &&
16
16
  config.settings.devProxyToApiPath &&
17
- req.headers.accept === 'application/json'
17
+ pathname.startsWith('/++api++')
18
18
  );
19
19
  };
20
20
 
@@ -79,9 +79,9 @@ export default function () {
79
79
  const { apiPathURL, instancePath } = getEnv();
80
80
  const target =
81
81
  config.settings.proxyRewriteTarget ||
82
- `/VirtualHostBase/http/${apiPathURL.hostname}:${apiPathURL.port}${instancePath}/VirtualHostRoot`;
82
+ `/VirtualHostBase/http/${apiPathURL.hostname}:${apiPathURL.port}${instancePath}/++api++/VirtualHostRoot`;
83
83
 
84
- return `${target}${path}`;
84
+ return `${target}${path.replace('/++api++', '')}`;
85
85
  },
86
86
  logLevel: process.env.DEBUG_HPM ? 'debug' : 'silent',
87
87
  ...(config.settings?.proxyRewriteTarget?.startsWith('https') && {
@@ -16,16 +16,18 @@ import config from '@plone/volto/registry';
16
16
  export const getAPIResourceWithAuth = (req) =>
17
17
  new Promise((resolve, reject) => {
18
18
  const { settings } = config;
19
+ const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
20
+
19
21
  let apiPath = '';
20
22
  if (settings.internalApiPath && __SERVER__) {
21
23
  apiPath = settings.internalApiPath;
22
- } else if (__DEVELOPMENT__ && config.settings.devProxyToApiPath) {
23
- apiPath = config.settings.devProxyToApiPath;
24
+ } else if (__DEVELOPMENT__ && settings.devProxyToApiPath) {
25
+ apiPath = settings.devProxyToApiPath;
24
26
  } else {
25
- apiPath = config.settings.apiPath;
27
+ apiPath = settings.apiPath;
26
28
  }
27
29
  const request = superagent
28
- .get(`${apiPath}${req.path}`)
30
+ .get(`${apiPath}${APISUFIX}${req.path}`)
29
31
  .maxResponseSize(settings.maxResponseSize)
30
32
  .responseType('blob');
31
33
  const authToken = cookie.load('auth_token');
@@ -17,16 +17,19 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
17
17
  */
18
18
  function formatUrl(path) {
19
19
  const { settings } = config;
20
+ const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
21
+
20
22
  if (path.startsWith('http://') || path.startsWith('https://')) return path;
21
23
 
22
24
  const adjustedPath = path[0] !== '/' ? `/${path}` : path;
23
25
  let apiPath = '';
24
26
  if (settings.internalApiPath && __SERVER__) {
25
27
  apiPath = settings.internalApiPath;
26
- } else if (config.settings.apiPath) {
27
- apiPath = config.settings.apiPath;
28
+ } else if (settings.apiPath) {
29
+ apiPath = settings.apiPath;
28
30
  }
29
- return `${apiPath}${adjustedPath}`;
31
+
32
+ return `${apiPath}${APISUFIX}${adjustedPath}`;
30
33
  }
31
34
 
32
35
  /**
@@ -0,0 +1,41 @@
1
+ import config from '@plone/volto/registry';
2
+ import Api from './Api';
3
+
4
+ jest.mock('superagent', () => ({
5
+ get: jest.fn((url) => ({
6
+ url,
7
+ query: jest.fn(),
8
+ set: jest.fn(),
9
+ type: jest.fn(),
10
+ send: jest.fn(),
11
+ end: jest.fn(),
12
+ })),
13
+ }));
14
+
15
+ beforeAll(() => {
16
+ config.settings.legacyTraverse = false;
17
+ });
18
+
19
+ const api = new Api();
20
+ const { settings } = config;
21
+
22
+ test('get request', () => {});
23
+
24
+ describe('Api', () => {
25
+ it('prefixes relative path', () => {
26
+ const promise = api.get('');
27
+ expect(promise.request.url).toBe(`${settings.apiPath}/++api++/`);
28
+ });
29
+ it('does not prefix absolute path', () => {
30
+ const promise = api.get('/test');
31
+ expect(promise.request.url).toBe(`${settings.apiPath}/++api++/test`);
32
+ });
33
+ it('does not change http URL provided as path', () => {
34
+ const promise = api.get('http://example.com');
35
+ expect(promise.request.url).toBe('http://example.com');
36
+ });
37
+ it('does not change https URL provided as path', () => {
38
+ const promise = api.get('https://example.com');
39
+ expect(promise.request.url).toBe('https://example.com');
40
+ });
41
+ });
@@ -18,6 +18,10 @@ jest.mock('superagent', () => ({
18
18
  })),
19
19
  }));
20
20
 
21
+ beforeAll(() => {
22
+ config.settings.legacyTraverse = true;
23
+ });
24
+
21
25
  const api = new Api();
22
26
  const { settings } = config;
23
27
 
package/src/server.jsx CHANGED
@@ -131,6 +131,14 @@ function setupServer(req, res, next) {
131
131
  .send(`<!doctype html> ${renderToString(errorPage)}`);
132
132
  }
133
133
 
134
+ if (!__DEVELOPMENT__ && !process.env.RAZZLE_API_PATH && req.headers.host) {
135
+ req.app.locals.detectedHost = `${
136
+ req.headers['x-forwarded-proto'] || req.protocol
137
+ }://${req.headers.host}`;
138
+ config.settings.apiPath = req.app.locals.detectedHost;
139
+ config.settings.publicURL = req.app.locals.detectedHost;
140
+ }
141
+
134
142
  req.app.locals = {
135
143
  ...req.app.locals,
136
144
  store,
@@ -153,16 +161,6 @@ server.get('/*', (req, res) => {
153
161
  const url = req.originalUrl || req.url;
154
162
  const location = parseUrl(url);
155
163
 
156
- let apiPathFromHostHeader;
157
- // Get the Host header as apiPath just in case that the apiPath is not set
158
- if (!config.settings.apiPath && req.headers.host) {
159
- apiPathFromHostHeader = `${
160
- req.headers['x-forwarded-proto'] || req.protocol
161
- }://${req.headers.host}`;
162
- config.settings.apiPath = apiPathFromHostHeader;
163
- config.settings.publicURL = apiPathFromHostHeader;
164
- }
165
-
166
164
  loadOnServer({ store, location, routes, api })
167
165
  .then(() => {
168
166
  // The content info is in the store at this point thanks to the asynconnect
@@ -208,8 +206,12 @@ server.get('/*', (req, res) => {
208
206
  process.env.NODE_ENV !== 'production'
209
207
  }
210
208
  criticalCss={readCriticalCss(req)}
211
- apiPath={apiPathFromHostHeader || config.settings.apiPath}
212
- publicURL={apiPathFromHostHeader || config.settings.publicURL}
209
+ apiPath={
210
+ req.app.locals.detectedHost || config.settings.apiPath
211
+ }
212
+ publicURL={
213
+ req.app.locals.detectedHost || config.settings.publicURL
214
+ }
213
215
  />,
214
216
  )}
215
217
  `,
@@ -223,8 +225,12 @@ server.get('/*', (req, res) => {
223
225
  markup={markup}
224
226
  store={store}
225
227
  criticalCss={readCriticalCss(req)}
226
- apiPath={apiPathFromHostHeader || config.settings.apiPath}
227
- publicURL={apiPathFromHostHeader || config.settings.publicURL}
228
+ apiPath={
229
+ req.app.locals.detectedHost || config.settings.apiPath
230
+ }
231
+ publicURL={
232
+ req.app.locals.detectedHost || config.settings.publicURL
233
+ }
228
234
  />,
229
235
  )}
230
236
  `,
@@ -40,13 +40,18 @@ export default () => {
40
40
  window.settings = config.settings;
41
41
  }
42
42
 
43
- // If Host header is present (so window.env.apiPath is)
43
+ // Setup the client registry from the SSR response values, presents in the `window.env`
44
+ // variable. This is key for the Seamless mode to work.
44
45
  if (window.env.apiPath) {
45
46
  config.settings.apiPath = window.env.apiPath;
46
47
  }
47
48
  if (window.env.publicURL) {
48
49
  config.settings.publicURL = window.env.publicURL;
49
50
  }
51
+ // TODO: To be removed when the use of the legacy traverse is deprecated.
52
+ if (window.env.RAZZLE_LEGACY_TRAVERSE) {
53
+ config.settings.legacyTraverse = true;
54
+ }
50
55
 
51
56
  loadableReady(() => {
52
57
  hydrate(