@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 +30 -2
- package/package.json +2 -2
- package/src/components/manage/Form/Field.jsx +20 -0
- package/src/components/manage/Form/Field.test.jsx +23 -0
- package/src/components/manage/Widgets/SelectWidget.jsx +16 -1
- package/src/components/manage/Widgets/VocabularyTermsWidget.jsx +225 -138
- package/src/components/manage/Widgets/VocabularyTermsWidget.stories.js +31 -32
- package/src/components/manage/Widgets/VocabularyTermsWidget.test.jsx +36 -4
- package/src/config/index.js +2 -0
- package/src/express-middleware/devproxy.js +3 -3
- package/src/helpers/Api/APIResourceWithAuth.js +6 -4
- package/src/helpers/Api/Api.js +6 -3
- package/src/helpers/Api/Api.plone.rest.test.js +41 -0
- package/src/helpers/Api/Api.test.js +4 -0
- package/src/server.jsx +20 -14
- package/src/start-client.jsx +6 -1
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
|
|
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.
|
|
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 (
|
|
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
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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 {
|
|
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,
|
|
91
|
+
import { Button, Segment } from 'semantic-ui-react';
|
|
30
92
|
|
|
31
|
-
import {
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
: keys(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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:
|
|
171
|
+
fields: supportedLanguages,
|
|
97
172
|
},
|
|
98
173
|
],
|
|
99
174
|
properties: Object.fromEntries(
|
|
100
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
<
|
|
158
|
-
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
</
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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="
|
|
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
|
|
56
|
+
const WrappedSimple = (args) => {
|
|
54
57
|
const [value, setValue] = React.useState({
|
|
55
|
-
'001':
|
|
56
|
-
|
|
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={
|
|
66
|
+
customStore={customStore}
|
|
72
67
|
>
|
|
73
68
|
<div className="ui segment form attached">
|
|
74
69
|
<VocabularyTermsWidget
|
|
75
70
|
{...args}
|
|
76
|
-
id="
|
|
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
|
);
|
package/src/config/index.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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__ &&
|
|
23
|
-
apiPath =
|
|
24
|
+
} else if (__DEVELOPMENT__ && settings.devProxyToApiPath) {
|
|
25
|
+
apiPath = settings.devProxyToApiPath;
|
|
24
26
|
} else {
|
|
25
|
-
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');
|
package/src/helpers/Api/Api.js
CHANGED
|
@@ -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 (
|
|
27
|
-
apiPath =
|
|
28
|
+
} else if (settings.apiPath) {
|
|
29
|
+
apiPath = settings.apiPath;
|
|
28
30
|
}
|
|
29
|
-
|
|
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
|
+
});
|
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={
|
|
212
|
-
|
|
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={
|
|
227
|
-
|
|
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
|
`,
|
package/src/start-client.jsx
CHANGED
|
@@ -40,13 +40,18 @@ export default () => {
|
|
|
40
40
|
window.settings = config.settings;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
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(
|