@plone/volto 18.0.0-alpha.43 → 18.0.0-alpha.45
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 +50 -0
- package/locales/ca/LC_MESSAGES/volto.po +5 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +5 -0
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +5 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +5 -0
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +5 -0
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +5 -0
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +5 -0
- package/locales/fr.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +5 -0
- package/locales/hi.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +42 -37
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +5 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +5 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +5 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +71 -66
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +5 -0
- package/locales/ro.json +1 -1
- package/locales/volto.pot +6 -1
- package/locales/zh_CN/LC_MESSAGES/volto.po +5 -0
- package/locales/zh_CN.json +1 -1
- package/package.json +6 -6
- package/src/components/manage/BlockChooser/BlockChooser.jsx +2 -2
- package/src/components/manage/BlockChooser/BlockChooser.test.jsx +4 -0
- package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +8 -1
- package/src/components/manage/Blocks/Search/components/CheckboxFacet.jsx +1 -0
- package/src/components/manage/Diff/DiffField.jsx +167 -39
- package/src/components/manage/Form/Form.jsx +31 -22
- package/src/components/manage/Sidebar/SidebarPopup.jsx +9 -1
- package/src/components/manage/Toast/Toast.jsx +2 -2
- package/src/components/manage/Widgets/ArrayWidget.jsx +1 -0
- package/src/components/manage/Widgets/FormFieldWrapper.jsx +23 -3
- package/src/components/manage/Widgets/SelectWidget.jsx +1 -0
- package/src/components/manage/Widgets/TokenWidget.jsx +1 -0
- package/src/config/NonContentRoutesPublic.jsx +15 -0
- package/src/config/index.js +2 -0
- package/src/helpers/Blocks/Blocks.js +1 -1
- package/src/helpers/Url/Url.js +4 -1
- package/src/helpers/Url/Url.test.js +17 -3
- package/src/hooks/index.js +1 -0
- package/src/hooks/user/useUser.js +23 -0
- package/test-setup-config.jsx +2 -0
- package/theme/themes/pastanaga/collections/form.overrides +1 -0
- package/theme/themes/pastanaga/collections/table.overrides +6 -1
- package/theme/themes/pastanaga/extras/main.less +3 -2
- package/theme/themes/pastanaga/extras/toolbar.less +14 -2
- package/theme/themes/pastanaga/extras/widgets.less +12 -0
- package/theme/themes/pastanaga/modules/accordion.variables +3 -3
- package/types/config/NonContentRoutesPublic.d.ts +1 -0
- package/types/helpers/Blocks/Blocks.d.ts +1 -1
- package/types/hooks/index.d.ts +1 -0
- package/types/hooks/user/useUser.d.ts +2 -0
- package/finalreleasechangelog.py +0 -48
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from 'react';
|
|
7
|
-
// import { diffWords as dWords } from 'diff';
|
|
8
7
|
import { join, map } from 'lodash';
|
|
9
8
|
import PropTypes from 'prop-types';
|
|
10
9
|
import { Grid } from 'semantic-ui-react';
|
|
@@ -13,20 +12,128 @@ import { Provider } from 'react-intl-redux';
|
|
|
13
12
|
import { createBrowserHistory } from 'history';
|
|
14
13
|
import { ConnectedRouter } from 'connected-react-router';
|
|
15
14
|
import { useSelector } from 'react-redux';
|
|
16
|
-
|
|
15
|
+
import config from '@plone/volto/registry';
|
|
17
16
|
import { Api } from '@plone/volto/helpers';
|
|
18
17
|
import configureStore from '@plone/volto/store';
|
|
19
|
-
import {
|
|
18
|
+
import { RenderBlocks } from '@plone/volto/components';
|
|
20
19
|
import { serializeNodes } from '@plone/volto-slate/editor/render';
|
|
21
|
-
|
|
22
20
|
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
const isHtmlTag = (str) => {
|
|
23
|
+
// Match complete HTML tags, including:
|
|
24
|
+
// 1. Opening tags like <div>, <img src="example" />, <svg>...</svg>
|
|
25
|
+
// 2. Self-closing tags like <img />, <br />
|
|
26
|
+
// 3. Closing tags like </div>
|
|
27
|
+
return /^<([a-zA-Z]+[0-9]*)\b[^>]*>|^<\/([a-zA-Z]+[0-9]*)\b[^>]*>$|^<([a-zA-Z]+[0-9]*)\b[^>]*\/>$/.test(
|
|
28
|
+
str,
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const splitWords = (str) => {
|
|
33
|
+
if (typeof str !== 'string') return str;
|
|
34
|
+
if (!str) return [];
|
|
35
|
+
|
|
36
|
+
const result = [];
|
|
37
|
+
let currentWord = '';
|
|
38
|
+
let insideTag = false;
|
|
39
|
+
let insideSpecialTag = false;
|
|
40
|
+
let tagBuffer = '';
|
|
41
|
+
|
|
42
|
+
// Special tags that should not be split (e.g., <img />, <svg> ... </svg>)
|
|
43
|
+
const specialTags = ['img', 'svg'];
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < str.length; i++) {
|
|
46
|
+
const char = str[i];
|
|
47
|
+
|
|
48
|
+
// Start of an HTML tag
|
|
49
|
+
if (char === '<') {
|
|
50
|
+
if (currentWord) {
|
|
51
|
+
result.push(currentWord); // Push text before the tag
|
|
52
|
+
currentWord = '';
|
|
53
|
+
}
|
|
54
|
+
insideTag = true;
|
|
55
|
+
tagBuffer += char;
|
|
56
|
+
}
|
|
57
|
+
// End of an HTML tag
|
|
58
|
+
else if (char === '>') {
|
|
59
|
+
tagBuffer += char;
|
|
60
|
+
insideTag = false;
|
|
61
|
+
|
|
62
|
+
// Check if the tagBuffer contains a special tag
|
|
63
|
+
const tagNameMatch = tagBuffer.match(/^<\/?([a-zA-Z]+[0-9]*)\b/);
|
|
64
|
+
if (tagNameMatch && specialTags.includes(tagNameMatch[1])) {
|
|
65
|
+
insideSpecialTag =
|
|
66
|
+
tagNameMatch[0].startsWith('<') && !tagNameMatch[0].startsWith('</');
|
|
67
|
+
result.push(tagBuffer); // Push the complete special tag as one unit
|
|
68
|
+
tagBuffer = '';
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
result.push(tagBuffer); // Push the complete tag
|
|
73
|
+
tagBuffer = '';
|
|
74
|
+
}
|
|
75
|
+
// Inside the tag or special tag
|
|
76
|
+
else if (insideTag || insideSpecialTag) {
|
|
77
|
+
tagBuffer += char;
|
|
78
|
+
}
|
|
79
|
+
// Space outside of tags - push current word
|
|
80
|
+
else if (char === ' ' && !insideTag && !insideSpecialTag) {
|
|
81
|
+
if (currentWord) {
|
|
82
|
+
result.push(currentWord);
|
|
83
|
+
currentWord = '';
|
|
84
|
+
}
|
|
85
|
+
result.push(' ');
|
|
86
|
+
} else if (
|
|
87
|
+
char === ',' &&
|
|
88
|
+
i < str.length - 1 &&
|
|
89
|
+
str[i + 1] !== ' ' &&
|
|
90
|
+
!insideTag &&
|
|
91
|
+
!insideSpecialTag
|
|
92
|
+
) {
|
|
93
|
+
if (currentWord) {
|
|
94
|
+
result.push(currentWord + char);
|
|
95
|
+
currentWord = '';
|
|
96
|
+
}
|
|
97
|
+
result.push(' ');
|
|
98
|
+
}
|
|
99
|
+
// Accumulate characters outside of tags
|
|
100
|
+
else {
|
|
101
|
+
currentWord += char;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Push any remaining text
|
|
106
|
+
if (currentWord) {
|
|
107
|
+
result.push(currentWord);
|
|
108
|
+
}
|
|
109
|
+
if (tagBuffer) {
|
|
110
|
+
result.push(tagBuffer); // Push remaining tagBuffer
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const formatDiffPart = (part, value, side) => {
|
|
117
|
+
if (!isHtmlTag(value)) {
|
|
118
|
+
if (part.removed && (side === 'left' || side === 'unified')) {
|
|
119
|
+
return `<span class="deletion">${value}</span>`;
|
|
120
|
+
} else if (part.removed) return '';
|
|
121
|
+
else if (part.added && (side === 'right' || side === 'unified')) {
|
|
122
|
+
return `<span class="addition">${value}</span>`;
|
|
123
|
+
} else if (part.added) return '';
|
|
124
|
+
return value;
|
|
125
|
+
} else {
|
|
126
|
+
if (side === 'unified' && part.added) return value;
|
|
127
|
+
else if (side === 'unified' && part.removed) return '';
|
|
128
|
+
if (part.removed && side === 'left') {
|
|
129
|
+
return value;
|
|
130
|
+
} else if (part.removed) return '';
|
|
131
|
+
else if (part.added && side === 'right') {
|
|
132
|
+
return value;
|
|
133
|
+
} else if (part.added) return '';
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
30
137
|
|
|
31
138
|
/**
|
|
32
139
|
* Diff field component.
|
|
@@ -36,6 +143,7 @@ import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
|
36
143
|
* @param {Object} schema Field schema
|
|
37
144
|
* @returns {string} Markup of the component.
|
|
38
145
|
*/
|
|
146
|
+
|
|
39
147
|
const DiffField = ({
|
|
40
148
|
one,
|
|
41
149
|
two,
|
|
@@ -51,7 +159,10 @@ const DiffField = ({
|
|
|
51
159
|
timeStyle: 'short',
|
|
52
160
|
};
|
|
53
161
|
const diffWords = (oneStr, twoStr) => {
|
|
54
|
-
return diffLib.
|
|
162
|
+
return diffLib.diffArrays(
|
|
163
|
+
splitWords(String(oneStr)),
|
|
164
|
+
splitWords(String(twoStr)),
|
|
165
|
+
);
|
|
55
166
|
};
|
|
56
167
|
|
|
57
168
|
let parts, oneArray, twoArray;
|
|
@@ -78,14 +189,14 @@ const DiffField = ({
|
|
|
78
189
|
ReactDOMServer.renderToStaticMarkup(
|
|
79
190
|
<Provider store={store}>
|
|
80
191
|
<ConnectedRouter history={history}>
|
|
81
|
-
<
|
|
192
|
+
<RenderBlocks content={contentOne} />
|
|
82
193
|
</ConnectedRouter>
|
|
83
194
|
</Provider>,
|
|
84
195
|
),
|
|
85
196
|
ReactDOMServer.renderToStaticMarkup(
|
|
86
197
|
<Provider store={store}>
|
|
87
198
|
<ConnectedRouter history={history}>
|
|
88
|
-
<
|
|
199
|
+
<RenderBlocks content={contentTwo} />
|
|
89
200
|
</ConnectedRouter>
|
|
90
201
|
</Provider>,
|
|
91
202
|
),
|
|
@@ -116,7 +227,30 @@ const DiffField = ({
|
|
|
116
227
|
}
|
|
117
228
|
case 'textarea':
|
|
118
229
|
default:
|
|
119
|
-
|
|
230
|
+
const Widget = config.widgets?.views?.widget?.[schema.widget];
|
|
231
|
+
|
|
232
|
+
if (Widget) {
|
|
233
|
+
const api = new Api();
|
|
234
|
+
const history = createBrowserHistory();
|
|
235
|
+
const store = configureStore(window.__data, history, api);
|
|
236
|
+
parts = diffWords(
|
|
237
|
+
ReactDOMServer.renderToStaticMarkup(
|
|
238
|
+
<Provider store={store}>
|
|
239
|
+
<ConnectedRouter history={history}>
|
|
240
|
+
<Widget value={one} />
|
|
241
|
+
</ConnectedRouter>
|
|
242
|
+
</Provider>,
|
|
243
|
+
),
|
|
244
|
+
ReactDOMServer.renderToStaticMarkup(
|
|
245
|
+
<Provider store={store}>
|
|
246
|
+
<ConnectedRouter history={history}>
|
|
247
|
+
<Widget value={two} />
|
|
248
|
+
</ConnectedRouter>
|
|
249
|
+
</Provider>,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
} else parts = diffWords(one, two);
|
|
253
|
+
|
|
120
254
|
break;
|
|
121
255
|
}
|
|
122
256
|
} else if (schema.type === 'object') {
|
|
@@ -128,6 +262,7 @@ const DiffField = ({
|
|
|
128
262
|
} else {
|
|
129
263
|
parts = diffWords(one?.title || one, two?.title || two);
|
|
130
264
|
}
|
|
265
|
+
|
|
131
266
|
return (
|
|
132
267
|
<Grid data-testid="DiffField">
|
|
133
268
|
<Grid.Row>
|
|
@@ -140,14 +275,12 @@ const DiffField = ({
|
|
|
140
275
|
<span
|
|
141
276
|
dangerouslySetInnerHTML={{
|
|
142
277
|
__html: join(
|
|
143
|
-
map(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
'',
|
|
150
|
-
),
|
|
278
|
+
map(parts, (part) => {
|
|
279
|
+
let combined = (part.value || []).reduce((acc, value) => {
|
|
280
|
+
return acc + formatDiffPart(part, value, 'left');
|
|
281
|
+
}, '');
|
|
282
|
+
return combined;
|
|
283
|
+
}),
|
|
151
284
|
'',
|
|
152
285
|
),
|
|
153
286
|
}}
|
|
@@ -157,14 +290,12 @@ const DiffField = ({
|
|
|
157
290
|
<span
|
|
158
291
|
dangerouslySetInnerHTML={{
|
|
159
292
|
__html: join(
|
|
160
|
-
map(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
'',
|
|
167
|
-
),
|
|
293
|
+
map(parts, (part) => {
|
|
294
|
+
let combined = (part.value || []).reduce((acc, value) => {
|
|
295
|
+
return acc + formatDiffPart(part, value, 'right');
|
|
296
|
+
}, '');
|
|
297
|
+
return combined;
|
|
298
|
+
}),
|
|
168
299
|
'',
|
|
169
300
|
),
|
|
170
301
|
}}
|
|
@@ -178,15 +309,12 @@ const DiffField = ({
|
|
|
178
309
|
<span
|
|
179
310
|
dangerouslySetInnerHTML={{
|
|
180
311
|
__html: join(
|
|
181
|
-
map(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
`<span class="addition">${part.value}</span>`) ||
|
|
188
|
-
(!part.added && `<span>${part.value}</span>`),
|
|
189
|
-
),
|
|
312
|
+
map(parts, (part) => {
|
|
313
|
+
let combined = (part.value || []).reduce((acc, value) => {
|
|
314
|
+
return acc + formatDiffPart(part, value, 'unified');
|
|
315
|
+
}, '');
|
|
316
|
+
return combined;
|
|
317
|
+
}),
|
|
190
318
|
'',
|
|
191
319
|
),
|
|
192
320
|
}}
|
|
@@ -274,19 +274,22 @@ class Form extends Component {
|
|
|
274
274
|
selected: null,
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
|
-
|
|
278
|
-
if (requestError && prevProps.requestError !== requestError) {
|
|
277
|
+
if (requestError) {
|
|
279
278
|
errors =
|
|
280
279
|
FormValidation.giveServerErrorsToCorrespondingFields(requestError);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
280
|
+
if (
|
|
281
|
+
!isEqual(prevProps.requestError, requestError) ||
|
|
282
|
+
!isEqual(this.state.errors, errors)
|
|
283
|
+
) {
|
|
284
|
+
activeIndex = FormValidation.showFirstTabWithErrors({
|
|
285
|
+
errors,
|
|
286
|
+
schema: this.props.schema,
|
|
287
|
+
});
|
|
288
|
+
this.setState({
|
|
289
|
+
errors,
|
|
290
|
+
activeIndex,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
if (this.props.onChangeFormData) {
|
|
@@ -563,13 +566,11 @@ class Form extends Component {
|
|
|
563
566
|
}
|
|
564
567
|
});
|
|
565
568
|
}
|
|
566
|
-
|
|
567
569
|
if (keys(errors).length > 0 || keys(blocksErrors).length > 0) {
|
|
568
570
|
const activeIndex = FormValidation.showFirstTabWithErrors({
|
|
569
571
|
errors,
|
|
570
572
|
schema: this.props.schema,
|
|
571
573
|
});
|
|
572
|
-
|
|
573
574
|
this.setState({
|
|
574
575
|
errors: {
|
|
575
576
|
...errors,
|
|
@@ -580,14 +581,23 @@ class Form extends Component {
|
|
|
580
581
|
|
|
581
582
|
if (keys(errors).length > 0) {
|
|
582
583
|
// Changes the focus to the metadata tab in the sidebar if error
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
584
|
+
toast.error(
|
|
585
|
+
<Toast
|
|
586
|
+
error
|
|
587
|
+
title={this.props.intl.formatMessage(messages.error)}
|
|
588
|
+
content={
|
|
589
|
+
<ul>
|
|
590
|
+
{Object.keys(errors).map((err, index) => (
|
|
591
|
+
<li key={index}>
|
|
592
|
+
<strong>
|
|
593
|
+
{this.props.schema.properties[err].title || err}:
|
|
594
|
+
</strong>{' '}
|
|
595
|
+
{errors[err]}
|
|
596
|
+
</li>
|
|
597
|
+
))}
|
|
598
|
+
</ul>
|
|
599
|
+
}
|
|
600
|
+
/>,
|
|
591
601
|
);
|
|
592
602
|
this.props.setSidebarTab(0);
|
|
593
603
|
} else if (keys(blocksErrors).length > 0) {
|
|
@@ -714,7 +724,6 @@ class Form extends Component {
|
|
|
714
724
|
const schema = this.removeBlocksLayoutFields(originalSchema);
|
|
715
725
|
const Container =
|
|
716
726
|
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
717
|
-
|
|
718
727
|
return this.props.visual ? (
|
|
719
728
|
// Removing this from SSR is important, since react-beautiful-dnd supports SSR,
|
|
720
729
|
// but draftJS don't like it much and the hydration gets messed up
|
|
@@ -16,6 +16,13 @@ const SidebarPopup = (props) => {
|
|
|
16
16
|
onClose();
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
const handleEscapeKey = (e) => {
|
|
20
|
+
if (open && e.key === 'Escape') {
|
|
21
|
+
onClose();
|
|
22
|
+
e.stopPropagation();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
19
26
|
const [isClient, setIsClient] = React.useState(false);
|
|
20
27
|
React.useEffect(() => {
|
|
21
28
|
setIsClient(true);
|
|
@@ -23,11 +30,12 @@ const SidebarPopup = (props) => {
|
|
|
23
30
|
|
|
24
31
|
React.useEffect(() => {
|
|
25
32
|
document.addEventListener('mousedown', handleClickOutside, false);
|
|
33
|
+
document.addEventListener('keyup', handleEscapeKey, false);
|
|
26
34
|
return () => {
|
|
27
35
|
document.removeEventListener('mousedown', handleClickOutside, false);
|
|
36
|
+
document.removeEventListener('keyup', handleEscapeKey, false);
|
|
28
37
|
};
|
|
29
38
|
});
|
|
30
|
-
|
|
31
39
|
return (
|
|
32
40
|
<>
|
|
33
41
|
{overlay && (
|
|
@@ -29,7 +29,7 @@ const Toast = (props) => {
|
|
|
29
29
|
<Icon name={getIcon(props)} size="18px" />
|
|
30
30
|
<div className="toast-inner-content">
|
|
31
31
|
{title && <h4>{title}</h4>}
|
|
32
|
-
<
|
|
32
|
+
<div>{content}</div>
|
|
33
33
|
</div>
|
|
34
34
|
</>
|
|
35
35
|
);
|
|
@@ -37,7 +37,7 @@ const Toast = (props) => {
|
|
|
37
37
|
|
|
38
38
|
Toast.propTypes = {
|
|
39
39
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
|
40
|
-
content: PropTypes.string.isRequired,
|
|
40
|
+
content: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
|
41
41
|
info: PropTypes.bool,
|
|
42
42
|
success: PropTypes.bool,
|
|
43
43
|
error: PropTypes.bool,
|
|
@@ -314,6 +314,7 @@ class ArrayWidget extends Component {
|
|
|
314
314
|
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
|
|
315
315
|
getHelperDimensions={({ node }) => node.getBoundingClientRect()}
|
|
316
316
|
id={`field-${this.props.id}`}
|
|
317
|
+
aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${this.props.id}`}
|
|
317
318
|
key={this.props.id}
|
|
318
319
|
isDisabled={this.props.disabled || this.props.isDisabled}
|
|
319
320
|
className="react-select-container"
|
|
@@ -8,6 +8,8 @@ import { Form, Grid, Icon as IconOld, Label } from 'semantic-ui-react';
|
|
|
8
8
|
import { map } from 'lodash';
|
|
9
9
|
import cx from 'classnames';
|
|
10
10
|
import { defineMessages, injectIntl } from 'react-intl';
|
|
11
|
+
import LanguageSVG from '@plone/volto/icons/language.svg';
|
|
12
|
+
import { Icon } from '@plone/volto/components';
|
|
11
13
|
|
|
12
14
|
const messages = defineMessages({
|
|
13
15
|
edit: {
|
|
@@ -22,6 +24,11 @@ const messages = defineMessages({
|
|
|
22
24
|
id: 'Language independent field.',
|
|
23
25
|
defaultMessage: 'Language independent field.',
|
|
24
26
|
},
|
|
27
|
+
language_independent_icon_title: {
|
|
28
|
+
id: 'Language independent icon title',
|
|
29
|
+
defaultMessage:
|
|
30
|
+
'This is a language independent field. Any value you enter here will overwrite the corresponding field of all members of the translation group when you save this form.',
|
|
31
|
+
},
|
|
25
32
|
});
|
|
26
33
|
/**
|
|
27
34
|
* FormFieldWrapper component class.
|
|
@@ -91,6 +98,9 @@ class FormFieldWrapper extends Component {
|
|
|
91
98
|
noForInFieldLabel,
|
|
92
99
|
multilingual_options,
|
|
93
100
|
} = this.props;
|
|
101
|
+
|
|
102
|
+
const languageIndependent = multilingual_options?.language_independent;
|
|
103
|
+
|
|
94
104
|
const wdg = (
|
|
95
105
|
<>
|
|
96
106
|
{this.props.children}
|
|
@@ -112,9 +122,7 @@ class FormFieldWrapper extends Component {
|
|
|
112
122
|
description ? 'help' : '',
|
|
113
123
|
className,
|
|
114
124
|
`field-wrapper-${id}`,
|
|
115
|
-
|
|
116
|
-
? 'language-independent-field'
|
|
117
|
-
: null,
|
|
125
|
+
languageIndependent ? 'language-independent-field' : null,
|
|
118
126
|
)}
|
|
119
127
|
>
|
|
120
128
|
<Grid>
|
|
@@ -133,6 +141,18 @@ class FormFieldWrapper extends Component {
|
|
|
133
141
|
/>
|
|
134
142
|
)}
|
|
135
143
|
{title}
|
|
144
|
+
{languageIndependent && (
|
|
145
|
+
<div className="languageIndependent-icon">
|
|
146
|
+
<Icon
|
|
147
|
+
title={intl.formatMessage(
|
|
148
|
+
messages.language_independent_icon_title,
|
|
149
|
+
)}
|
|
150
|
+
name={LanguageSVG}
|
|
151
|
+
size="24px"
|
|
152
|
+
color="#555"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
136
156
|
</label>
|
|
137
157
|
</div>
|
|
138
158
|
</Grid.Column>
|
|
@@ -224,6 +224,7 @@ class SelectWidget extends Component {
|
|
|
224
224
|
id={`field-${id}`}
|
|
225
225
|
key={choices}
|
|
226
226
|
name={id}
|
|
227
|
+
aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${id}`}
|
|
227
228
|
menuShouldScrollIntoView={false}
|
|
228
229
|
isDisabled={disabled}
|
|
229
230
|
isSearchable={true}
|
|
@@ -173,6 +173,7 @@ class TokenWidget extends Component {
|
|
|
173
173
|
<FormFieldWrapper {...this.props}>
|
|
174
174
|
<CreatableSelect
|
|
175
175
|
id={`field-${this.props.id}`}
|
|
176
|
+
aria-labelledby={`fieldset-${this.props.fieldSet}-field-label-${this.props.id}`}
|
|
176
177
|
key={this.props.id}
|
|
177
178
|
menuShouldScrollIntoView={false}
|
|
178
179
|
isDisabled={this.props.isDisabled}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// PublicUi routes that are nonContentRoutes, and should not be members of isCmsUi
|
|
2
|
+
// You can include either RegEx or a string representing the ending of the
|
|
3
|
+
// nonContentRoute. For example, '/add' will match '/foo/bar/add'.
|
|
4
|
+
export const nonContentRoutesPublic = [
|
|
5
|
+
'/login',
|
|
6
|
+
'/logout',
|
|
7
|
+
'/sitemap',
|
|
8
|
+
'/register',
|
|
9
|
+
'/search',
|
|
10
|
+
'/change-password',
|
|
11
|
+
'/contact-form',
|
|
12
|
+
'/register',
|
|
13
|
+
/\/passwordreset\/.*$/,
|
|
14
|
+
'/passwordreset',
|
|
15
|
+
];
|
package/src/config/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
layoutViewsNamesMapping,
|
|
9
9
|
} from './Views';
|
|
10
10
|
import { nonContentRoutes } from './NonContentRoutes';
|
|
11
|
+
import { nonContentRoutesPublic } from './NonContentRoutesPublic';
|
|
11
12
|
import {
|
|
12
13
|
groupBlocksOrder,
|
|
13
14
|
requiredBlocks,
|
|
@@ -113,6 +114,7 @@ let config = {
|
|
|
113
114
|
legacyTraverse: process.env.RAZZLE_LEGACY_TRAVERSE || false,
|
|
114
115
|
cookieExpires: 15552000, //in seconds. Default is 6 month (15552000)
|
|
115
116
|
nonContentRoutes,
|
|
117
|
+
nonContentRoutesPublic,
|
|
116
118
|
imageObjects: ['Image'],
|
|
117
119
|
reservedIds: ['login', 'layout', 'plone', 'zip', 'properties'],
|
|
118
120
|
downloadableObjects: ['File'], //list of content-types for which the direct download of the file will be carried out if the user is not authenticated
|
|
@@ -718,7 +718,7 @@ export function isBlockContainer(block) {
|
|
|
718
718
|
* @param {Object} types A list with the list of types to be matched
|
|
719
719
|
* @return {Array} An array of block ids
|
|
720
720
|
*/
|
|
721
|
-
export function findBlocks(blocks, types, result = []) {
|
|
721
|
+
export function findBlocks(blocks = {}, types, result = []) {
|
|
722
722
|
Object.keys(blocks).forEach((blockId) => {
|
|
723
723
|
const block = blocks[blockId];
|
|
724
724
|
// check blocks from data as well since some add-ons use that
|
package/src/helpers/Url/Url.js
CHANGED
|
@@ -139,7 +139,10 @@ export const isCmsUi = memoize((currentPathname) => {
|
|
|
139
139
|
// because the regexp test does not take that into account
|
|
140
140
|
// https://github.com/plone/volto/issues/870
|
|
141
141
|
return settings.nonContentRoutes.reduce(
|
|
142
|
-
(acc, route) =>
|
|
142
|
+
(acc, route) =>
|
|
143
|
+
acc ||
|
|
144
|
+
(!settings.nonContentRoutesPublic?.includes(route) &&
|
|
145
|
+
new RegExp(route).test(fullPath)),
|
|
143
146
|
false,
|
|
144
147
|
);
|
|
145
148
|
});
|
|
@@ -151,15 +151,29 @@ describe('Url', () => {
|
|
|
151
151
|
describe('isCmsUi', () => {
|
|
152
152
|
[...settings.nonContentRoutes, '/controlpanel/mypanel'].forEach((route) => {
|
|
153
153
|
if (typeof route === 'string') {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
if (settings.nonContentRoutesPublic.includes(route)) {
|
|
155
|
+
it(`matches non-content-route-public ${route}`, () => {
|
|
156
|
+
expect(isCmsUi(route)).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
it(`matches non-content-route ${route}`, () => {
|
|
160
|
+
expect(isCmsUi(`/mycontent/${route}`)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
164
|
});
|
|
159
165
|
|
|
160
166
|
it('returns false on non-cms-ui views', () => {
|
|
161
167
|
expect(isCmsUi('/mycontent')).toBe(false);
|
|
162
168
|
});
|
|
169
|
+
|
|
170
|
+
it('returns true on non content routes', () => {
|
|
171
|
+
expect(isCmsUi('/mycontent/historyview')).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns false on public non content routes', () => {
|
|
175
|
+
expect(isCmsUi('/mycontent/login')).toBe(false);
|
|
176
|
+
});
|
|
163
177
|
});
|
|
164
178
|
|
|
165
179
|
describe('flattenHTMLToAppURL', () => {
|
package/src/hooks/index.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
3
|
+
import jwtDecode from 'jwt-decode';
|
|
4
|
+
import { getUser } from '@plone/volto/actions';
|
|
5
|
+
|
|
6
|
+
const useUser = () => {
|
|
7
|
+
const users = useSelector((state) => state.users);
|
|
8
|
+
const user = users?.user;
|
|
9
|
+
const userId = useSelector((state) =>
|
|
10
|
+
state.userSession.token ? jwtDecode(state.userSession.token).sub : '',
|
|
11
|
+
);
|
|
12
|
+
const dispatch = useDispatch();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!user?.id && users?.get.loading === false) {
|
|
16
|
+
dispatch(getUser(userId));
|
|
17
|
+
}
|
|
18
|
+
}, [dispatch, userId, user, users?.get.loading]);
|
|
19
|
+
|
|
20
|
+
return user;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default useUser;
|
package/test-setup-config.jsx
CHANGED
|
@@ -10,6 +10,7 @@ import React from 'react';
|
|
|
10
10
|
import config from '@plone/volto/registry';
|
|
11
11
|
import { loadables } from '@plone/volto/config/Loadables';
|
|
12
12
|
import { nonContentRoutes } from '@plone/volto/config/NonContentRoutes';
|
|
13
|
+
import { nonContentRoutesPublic } from '@plone/volto/config/NonContentRoutesPublic';
|
|
13
14
|
import { contentIcons } from '@plone/volto/config/ContentIcons';
|
|
14
15
|
import {
|
|
15
16
|
styleClassNameConverters,
|
|
@@ -35,6 +36,7 @@ config.set('settings', {
|
|
|
35
36
|
showTags: true,
|
|
36
37
|
isMultilingual: false,
|
|
37
38
|
nonContentRoutes,
|
|
39
|
+
nonContentRoutesPublic,
|
|
38
40
|
contentIcons: contentIcons,
|
|
39
41
|
loadables,
|
|
40
42
|
lazyBundles: {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/* Headers */
|
|
9
|
-
.ui.table th {
|
|
9
|
+
.ui.table thead th {
|
|
10
10
|
padding: @headerVerticalPadding @headerHorizontalPadding;
|
|
11
11
|
border-left: @headerDivider;
|
|
12
12
|
background: @headerBackground;
|
|
@@ -34,3 +34,8 @@
|
|
|
34
34
|
.ui.table tr:first-child > th:only-child {
|
|
35
35
|
border-radius: @borderRadius @borderRadius 0em 0em;
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/* inline link color*/
|
|
39
|
+
.ui.table.inverted a {
|
|
40
|
+
color: @lightPrimaryColor;
|
|
41
|
+
}
|