@plone/volto 16.21.3 → 16.22.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.
- package/.changelog.draft +21 -4
- package/.gitignore~ +71 -0
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +60 -1
- package/cypress/support/commands.js +17 -0
- package/locales/ca/LC_MESSAGES/volto.po +37 -2
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +38 -3
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +37 -2
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +37 -2
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +37 -2
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +37 -2
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +37 -2
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +229 -194
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +37 -2
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +37 -2
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +37 -2
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +37 -2
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +37 -2
- package/locales/ro.json +1 -1
- package/locales/volto.pot +38 -3
- package/locales/volto.pot~ +4705 -0
- package/locales/zh_CN/LC_MESSAGES/volto.po +37 -2
- package/locales/zh_CN.json +1 -1
- package/news/4547.breaking~ +1 -0
- package/package.json +1 -1
- package/package.json~ +444 -0
- package/packages/volto-slate/package.json +1 -1
- package/packages/volto-slate/src/blocks/Table/TableBlockEdit.jsx +21 -212
- package/packages/volto-slate/src/blocks/Table/schema.js +122 -0
- package/packages/volto-slate/src/editor/plugins/StyleMenu/utils.js +14 -5
- package/packages/volto-slate/src/utils/blocks.js +7 -0
- package/packages/volto-slate/src/widgets/RichTextWidget.jsx +15 -8
- package/src/components/manage/Blocks/Image/schema.js +5 -1
- package/src/components/manage/Blocks/Search/components/Facets.jsx +6 -2
- package/src/components/manage/Blocks/Search/components/SearchInput.jsx +9 -2
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +12 -1
- package/src/components/manage/Blocks/ToC/Schema.jsx +40 -7
- package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +142 -8
- package/src/components/manage/Contents/Contents.jsx +8 -6
- package/src/components/manage/Contents/ContentsPropertiesModal.jsx +1 -13
- package/src/components/manage/Controlpanels/Groups/RenderGroups.jsx +2 -2
- package/src/components/manage/Controlpanels/Users/RenderUsers.jsx +2 -2
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +25 -10
- package/src/components/manage/UniversalLink/UniversalLink.jsx +2 -6
- package/src/components/manage/UniversalLink/UniversalLink.test.jsx +36 -0
- package/src/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +1 -1
- package/src/components/manage/Widgets/SelectUtils.js +1 -1
- package/src/components/theme/Error/ServerError.jsx +29 -0
- package/src/components/theme/Login/Login.jsx +1 -1
- package/src/components/theme/View/AlbumView.jsx +9 -1
- package/src/components/theme/View/EventView.jsx +6 -2
- package/src/components/theme/View/FileView.jsx +23 -18
- package/src/components/theme/View/ImageView.jsx +37 -32
- package/src/components/theme/View/LinkView.jsx +4 -1
- package/src/components/theme/View/ListingView.jsx +33 -27
- package/src/components/theme/View/SummaryView.jsx +47 -38
- package/src/components/theme/View/TabularView.jsx +59 -53
- package/src/config/Views.jsx +2 -0
- package/src/config/index.js +1 -0
- package/src/config/index.js~ +223 -0
- package/src/config/server.js +2 -0
- package/src/express-middleware/files.js +8 -6
- package/src/express-middleware/images.js +7 -1
- package/src/express-middleware/ok.js +16 -0
- package/src/helpers/Url/Url.js +22 -1
- package/src/helpers/Url/Url.test.js +41 -0
- package/src/middleware/api.js +14 -2
- package/src/reducers/actions/actions.js +7 -5
- package/src/reducers/actions/actions.test.js +70 -0
- package/src/server.jsx +9 -0
- package/theme/themes/pastanaga/extras/main.less +2 -1
- package/theme/themes/pastanaga/extras/toc.less +29 -0
- package/packages/volto-slate/build/messages/src/blocks/Table/TableBlockEdit.json +0 -90
- package/packages/volto-slate/build/messages/src/blocks/Text/DefaultTextBlockEditor.json +0 -6
- package/packages/volto-slate/build/messages/src/blocks/Text/DetachedTextBlockEditor.json +0 -6
- package/packages/volto-slate/build/messages/src/blocks/Text/SlashMenu.json +0 -6
- package/packages/volto-slate/build/messages/src/editor/plugins/AdvancedLink/index.json +0 -10
- package/packages/volto-slate/build/messages/src/editor/plugins/Link/index.json +0 -10
- package/packages/volto-slate/build/messages/src/editor/plugins/Table/index.json +0 -30
- package/packages/volto-slate/build/messages/src/elementEditor/messages.json +0 -10
- package/packages/volto-slate/build/messages/src/widgets/HtmlSlateWidget.json +0 -6
- package/packages/volto-slate/build/messages/src/widgets/RichTextWidgetView.json +0 -6
- package/pyvenv.cfg +0 -3
- package/share/man/man1/ttx.1 +0 -225
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
* View toc block.
|
|
3
|
-
* @module components/manage/Blocks/ToC/View
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
7
2
|
import PropTypes from 'prop-types';
|
|
8
3
|
import { map } from 'lodash';
|
|
9
|
-
import { Menu } from 'semantic-ui-react';
|
|
4
|
+
import { Menu, Dropdown } from 'semantic-ui-react';
|
|
10
5
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
11
6
|
import AnchorLink from 'react-anchor-link-smooth-scroll';
|
|
12
7
|
|
|
@@ -32,6 +27,131 @@ const RenderMenuItems = ({ items }) => {
|
|
|
32
27
|
* @extends Component
|
|
33
28
|
*/
|
|
34
29
|
const View = ({ data, tocEntries }) => {
|
|
30
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
31
|
+
// When the page is resized to prevent items from the TOC from going out of the viewport,
|
|
32
|
+
// a dropdown menu is added containing all the items that don't fit.
|
|
33
|
+
const handleResize = () => {
|
|
34
|
+
const menuElement = document.querySelector('.responsive-menu');
|
|
35
|
+
const containerWidth = menuElement.offsetWidth;
|
|
36
|
+
|
|
37
|
+
// Get all divs that contain the items from the TOC, except the dropdown button
|
|
38
|
+
const nested = document.querySelectorAll(
|
|
39
|
+
'.responsive-menu .item:not(.dropdown)',
|
|
40
|
+
);
|
|
41
|
+
const nestedArray = Object.values(nested);
|
|
42
|
+
const middle = Math.ceil(nestedArray.length / 2);
|
|
43
|
+
const firstHalfNested = nestedArray.slice(0, middle);
|
|
44
|
+
const secondHalfNested = nestedArray.slice(middle);
|
|
45
|
+
|
|
46
|
+
const dropdown = document.querySelector('.dropdown');
|
|
47
|
+
const dropdownWidth = dropdown.offsetWidth;
|
|
48
|
+
|
|
49
|
+
const firstHalfNestedHiddenItems = [];
|
|
50
|
+
|
|
51
|
+
// Add a 'hidden' class for the items that should be in the dropdown
|
|
52
|
+
firstHalfNested.forEach((item) => {
|
|
53
|
+
const itemOffsetLeft = item.offsetLeft;
|
|
54
|
+
const itemOffsetWidth = item.offsetWidth;
|
|
55
|
+
if (itemOffsetLeft + itemOffsetWidth > containerWidth - dropdownWidth) {
|
|
56
|
+
item.classList.add('hidden');
|
|
57
|
+
firstHalfNestedHiddenItems.push(item);
|
|
58
|
+
} else {
|
|
59
|
+
item.classList.remove('hidden');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
secondHalfNested.forEach((item) => item.classList.add('hidden-dropdown'));
|
|
64
|
+
|
|
65
|
+
const diff = firstHalfNested.length - firstHalfNestedHiddenItems.length;
|
|
66
|
+
const secondHalfNestedShownItems = secondHalfNested.slice(diff);
|
|
67
|
+
secondHalfNestedShownItems.forEach((item) =>
|
|
68
|
+
item.classList.remove('hidden-dropdown'),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// If there are elements that should be displayed in the dropdown, show the dropdown button
|
|
72
|
+
if (secondHalfNestedShownItems.length > 0)
|
|
73
|
+
dropdown.classList.remove('hidden-dropdown');
|
|
74
|
+
else {
|
|
75
|
+
dropdown.classList.add('hidden-dropdown');
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleDropdownKeyDown = (event) => {
|
|
80
|
+
const dropdownMenu = document.querySelector('.menu.transition');
|
|
81
|
+
if (event.key === 'ArrowDown' && isDropdownOpen) {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
const menuItems = dropdownMenu.querySelectorAll(
|
|
84
|
+
'.item:not(.hidden-dropdown)',
|
|
85
|
+
);
|
|
86
|
+
const focusedItem = dropdownMenu.querySelector('.item.focused');
|
|
87
|
+
const focusedIndex = Array.from(menuItems).indexOf(focusedItem);
|
|
88
|
+
|
|
89
|
+
if (focusedIndex === -1) {
|
|
90
|
+
// No item is currently focused, so focus the first item
|
|
91
|
+
menuItems[0].classList.add('focused');
|
|
92
|
+
} else if (focusedIndex === menuItems.length - 1) {
|
|
93
|
+
// Remove focus from the currently focused item and close the dropdown
|
|
94
|
+
focusedItem.classList.remove('focused');
|
|
95
|
+
setIsDropdownOpen(false);
|
|
96
|
+
|
|
97
|
+
// Focus the next element on the page
|
|
98
|
+
const nextElement = dropdownMenu.nextElementSibling;
|
|
99
|
+
if (nextElement) {
|
|
100
|
+
nextElement.focus();
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// Remove focus from the currently focused item
|
|
104
|
+
focusedItem.classList.remove('focused');
|
|
105
|
+
|
|
106
|
+
// Focus the next item or wrap around to the first item
|
|
107
|
+
const nextIndex = (focusedIndex + 1) % menuItems.length;
|
|
108
|
+
menuItems[nextIndex].classList.add('focused');
|
|
109
|
+
}
|
|
110
|
+
} else if (event.key === 'Enter' && isDropdownOpen) {
|
|
111
|
+
const focusedItem = dropdownMenu.querySelector('.item.focused');
|
|
112
|
+
if (focusedItem) {
|
|
113
|
+
focusedItem.querySelector('a').click();
|
|
114
|
+
focusedItem.classList.remove('focused');
|
|
115
|
+
}
|
|
116
|
+
} else if (event.key === 'Tab') {
|
|
117
|
+
const focusedItem = dropdownMenu.querySelector('.item.focused');
|
|
118
|
+
if (focusedItem) {
|
|
119
|
+
focusedItem.classList.remove('focused');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (data.sticky) {
|
|
126
|
+
const toc = document.querySelector('.horizontalMenu');
|
|
127
|
+
const tocPos = toc ? toc.offsetTop : 0;
|
|
128
|
+
|
|
129
|
+
const handleScroll = () => {
|
|
130
|
+
let scrollPos = window.scrollY;
|
|
131
|
+
if (scrollPos > tocPos && toc) {
|
|
132
|
+
toc.classList.add('sticky-toc');
|
|
133
|
+
} else if (scrollPos <= tocPos && toc) {
|
|
134
|
+
toc.classList.remove('sticky-toc');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
window.addEventListener('scroll', handleScroll);
|
|
139
|
+
|
|
140
|
+
return () => {
|
|
141
|
+
window.removeEventListener('scroll', handleScroll);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}, [data.sticky]);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
handleResize();
|
|
148
|
+
window.addEventListener('resize', handleResize);
|
|
149
|
+
|
|
150
|
+
return () => {
|
|
151
|
+
window.removeEventListener('resize', handleResize);
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
35
155
|
return (
|
|
36
156
|
<>
|
|
37
157
|
{data.title && !data.hide_title ? (
|
|
@@ -46,8 +166,22 @@ const View = ({ data, tocEntries }) => {
|
|
|
46
166
|
) : (
|
|
47
167
|
''
|
|
48
168
|
)}
|
|
49
|
-
<Menu>
|
|
169
|
+
<Menu className="responsive-menu">
|
|
50
170
|
<RenderMenuItems items={tocEntries} />
|
|
171
|
+
<Dropdown
|
|
172
|
+
item
|
|
173
|
+
text="More"
|
|
174
|
+
className="hidden-dropdown"
|
|
175
|
+
open={isDropdownOpen}
|
|
176
|
+
onOpen={() => setIsDropdownOpen(true)}
|
|
177
|
+
onClose={() => setIsDropdownOpen(false)}
|
|
178
|
+
tabIndex={0}
|
|
179
|
+
onKeyDown={handleDropdownKeyDown}
|
|
180
|
+
>
|
|
181
|
+
<Dropdown.Menu>
|
|
182
|
+
<RenderMenuItems items={tocEntries} />
|
|
183
|
+
</Dropdown.Menu>
|
|
184
|
+
</Dropdown>
|
|
51
185
|
</Menu>
|
|
52
186
|
</>
|
|
53
187
|
);
|
|
@@ -450,7 +450,7 @@ class Contents extends Component {
|
|
|
450
450
|
sort_on: this.props.sort?.on || 'getObjPositionInParent',
|
|
451
451
|
sort_order: this.props.sort?.order || 'ascending',
|
|
452
452
|
isClient: false,
|
|
453
|
-
linkIntegrityBreakages:
|
|
453
|
+
linkIntegrityBreakages: [],
|
|
454
454
|
};
|
|
455
455
|
this.filterTimeout = null;
|
|
456
456
|
}
|
|
@@ -1236,7 +1236,10 @@ class Contents extends Component {
|
|
|
1236
1236
|
Show all items
|
|
1237
1237
|
</Button>
|
|
1238
1238
|
)}
|
|
1239
|
-
{this.state.linkIntegrityBreakages.
|
|
1239
|
+
{this.state.linkIntegrityBreakages.reduce(
|
|
1240
|
+
(a, b) => a + b.breaches.length,
|
|
1241
|
+
0,
|
|
1242
|
+
) ? (
|
|
1240
1243
|
<div>
|
|
1241
1244
|
<h3>
|
|
1242
1245
|
{this.props.intl.formatMessage(
|
|
@@ -1249,9 +1252,8 @@ class Contents extends Component {
|
|
|
1249
1252
|
)}
|
|
1250
1253
|
</p>
|
|
1251
1254
|
<ul className="content">
|
|
1252
|
-
{map(
|
|
1253
|
-
|
|
1254
|
-
(item) => (
|
|
1255
|
+
{map(this.state.linkIntegrityBreakages, (item) =>
|
|
1256
|
+
item.breaches.length ? (
|
|
1255
1257
|
<li key={item['@id']}>
|
|
1256
1258
|
<a href={item['@id']}>{item.title}</a>
|
|
1257
1259
|
<p>
|
|
@@ -1269,7 +1271,7 @@ class Contents extends Component {
|
|
|
1269
1271
|
))}
|
|
1270
1272
|
</ul>
|
|
1271
1273
|
</li>
|
|
1272
|
-
),
|
|
1274
|
+
) : null,
|
|
1273
1275
|
)}
|
|
1274
1276
|
</ul>
|
|
1275
1277
|
</div>
|
|
@@ -70,14 +70,6 @@ const messages = defineMessages({
|
|
|
70
70
|
defaultMessage:
|
|
71
71
|
'If selected, this item will not appear in the navigation tree',
|
|
72
72
|
},
|
|
73
|
-
yes: {
|
|
74
|
-
id: 'Yes',
|
|
75
|
-
defaultMessage: 'Yes',
|
|
76
|
-
},
|
|
77
|
-
no: {
|
|
78
|
-
id: 'No',
|
|
79
|
-
defaultMessage: 'No',
|
|
80
|
-
},
|
|
81
73
|
});
|
|
82
74
|
|
|
83
75
|
/**
|
|
@@ -209,11 +201,7 @@ class ContentsPropertiesModal extends Component {
|
|
|
209
201
|
title: this.props.intl.formatMessage(
|
|
210
202
|
messages.excludeFromNavTitle,
|
|
211
203
|
),
|
|
212
|
-
type: '
|
|
213
|
-
choices: [
|
|
214
|
-
[true, this.props.intl.formatMessage(messages.yes)],
|
|
215
|
-
[false, this.props.intl.formatMessage(messages.no)],
|
|
216
|
-
],
|
|
204
|
+
type: 'boolean',
|
|
217
205
|
},
|
|
218
206
|
},
|
|
219
207
|
required: [],
|
|
@@ -57,7 +57,7 @@ class RenderGroups extends Component {
|
|
|
57
57
|
* @memberof UsersControlpanelUser
|
|
58
58
|
*/
|
|
59
59
|
onChange(event, { value }) {
|
|
60
|
-
const [group, role] = value.split('
|
|
60
|
+
const [group, role] = value.split('&role=');
|
|
61
61
|
this.props.updateGroups(group, role);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -97,7 +97,7 @@ class RenderGroups extends Component {
|
|
|
97
97
|
: this.props.group.roles.includes(role.id)
|
|
98
98
|
}
|
|
99
99
|
onChange={this.onChange}
|
|
100
|
-
value={`${this.props.group.id}
|
|
100
|
+
value={`${this.props.group.id}&role=${role.id}`}
|
|
101
101
|
/>
|
|
102
102
|
)}
|
|
103
103
|
</Table.Cell>
|
|
@@ -53,7 +53,7 @@ class RenderUsers extends Component {
|
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
55
|
onChange(event, { value }) {
|
|
56
|
-
const [user, role] = value.split('
|
|
56
|
+
const [user, role] = value.split('&role=');
|
|
57
57
|
this.props.updateUser(user, role);
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
@@ -83,7 +83,7 @@ class RenderUsers extends Component {
|
|
|
83
83
|
<Checkbox
|
|
84
84
|
checked={this.props.user.roles.includes(role.id)}
|
|
85
85
|
onChange={this.onChange}
|
|
86
|
-
value={`${this.props.user.id}
|
|
86
|
+
value={`${this.props.user.id}&role=${role.id}`}
|
|
87
87
|
/>
|
|
88
88
|
)}
|
|
89
89
|
</Table.Cell>
|
|
@@ -107,6 +107,7 @@ class UsersControlpanel extends Component {
|
|
|
107
107
|
isClient: false,
|
|
108
108
|
currentPage: 0,
|
|
109
109
|
pageSize: 10,
|
|
110
|
+
loginUsingEmail: false,
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -122,6 +123,14 @@ class UsersControlpanel extends Component {
|
|
|
122
123
|
}
|
|
123
124
|
};
|
|
124
125
|
|
|
126
|
+
// Because username field needs to be disabled if email login is enabled!
|
|
127
|
+
checkLoginUsingEmailStatus = async () => {
|
|
128
|
+
await this.props.getControlpanel('security');
|
|
129
|
+
this.setState({
|
|
130
|
+
loginUsingEmail: this.props.controlPanelData?.data.use_email_as_login,
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
125
134
|
/**
|
|
126
135
|
* Component did mount
|
|
127
136
|
* @method componentDidMount
|
|
@@ -132,6 +141,7 @@ class UsersControlpanel extends Component {
|
|
|
132
141
|
isClient: true,
|
|
133
142
|
});
|
|
134
143
|
this.fetchData();
|
|
144
|
+
this.checkLoginUsingEmailStatus();
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
@@ -425,7 +435,7 @@ class UsersControlpanel extends Component {
|
|
|
425
435
|
id: 'default',
|
|
426
436
|
title: 'FIXME: User Data',
|
|
427
437
|
fields: [
|
|
428
|
-
'username',
|
|
438
|
+
...(!this.state.loginUsingEmail ? ['username'] : []),
|
|
429
439
|
'fullname',
|
|
430
440
|
'email',
|
|
431
441
|
'password',
|
|
@@ -436,15 +446,19 @@ class UsersControlpanel extends Component {
|
|
|
436
446
|
},
|
|
437
447
|
],
|
|
438
448
|
properties: {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
449
|
+
...(!this.state.loginUsingEmail
|
|
450
|
+
? {
|
|
451
|
+
username: {
|
|
452
|
+
title: this.props.intl.formatMessage(
|
|
453
|
+
messages.addUserFormUsernameTitle,
|
|
454
|
+
),
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: this.props.intl.formatMessage(
|
|
457
|
+
messages.addUserFormUsernameDescription,
|
|
458
|
+
),
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
: {}),
|
|
448
462
|
fullname: {
|
|
449
463
|
title: this.props.intl.formatMessage(
|
|
450
464
|
messages.addUserFormFullnameTitle,
|
|
@@ -670,6 +684,7 @@ export default compose(
|
|
|
670
684
|
createRequest: state.users.create,
|
|
671
685
|
loadRolesRequest: state.roles,
|
|
672
686
|
inheritedRole: state.authRole.authenticatedRole,
|
|
687
|
+
controlPanelData: state.controlpanels?.controlpanel,
|
|
673
688
|
}),
|
|
674
689
|
(dispatch) =>
|
|
675
690
|
bindActionCreators(
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
isInternalURL,
|
|
13
13
|
URLUtils,
|
|
14
14
|
} from '@plone/volto/helpers/Url/Url';
|
|
15
|
-
import { matchPath } from 'react-router';
|
|
16
15
|
|
|
17
16
|
import config from '@plone/volto/registry';
|
|
18
17
|
|
|
@@ -66,11 +65,8 @@ const UniversalLink = ({
|
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
matchPath(flattenToAppURL(url), route.match),
|
|
72
|
-
)?.length > 0;
|
|
73
|
-
const isExternal = !isInternalURL(url) || isBlacklisted;
|
|
68
|
+
const isExternal = !isInternalURL(url);
|
|
69
|
+
|
|
74
70
|
const isDownload = (!isExternal && url.includes('@@download')) || download;
|
|
75
71
|
const isDisplayFile =
|
|
76
72
|
(!isExternal && url.includes('@@display-file')) || false;
|
|
@@ -157,6 +157,42 @@ describe('UniversalLink', () => {
|
|
|
157
157
|
);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
+
it('UniversalLink renders external link where link is blacklisted', () => {
|
|
161
|
+
const notInEN = /^(?!.*(#|\/en|\/static|\/controlpanel|\/cypress|\/login|\/logout|\/contact-form)).*$/;
|
|
162
|
+
config.settings.externalRoutes = [
|
|
163
|
+
{
|
|
164
|
+
match: {
|
|
165
|
+
path: notInEN,
|
|
166
|
+
exact: false,
|
|
167
|
+
strict: false,
|
|
168
|
+
},
|
|
169
|
+
url(payload) {
|
|
170
|
+
return payload.location.pathname;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const { getByTitle } = render(
|
|
176
|
+
<Provider store={store}>
|
|
177
|
+
<MemoryRouter>
|
|
178
|
+
<UniversalLink
|
|
179
|
+
href="http://localhost:3000/blacklisted-app"
|
|
180
|
+
title="External blacklisted app"
|
|
181
|
+
>
|
|
182
|
+
<h1>Title</h1>
|
|
183
|
+
</UniversalLink>
|
|
184
|
+
</MemoryRouter>
|
|
185
|
+
</Provider>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(getByTitle('External blacklisted app').getAttribute('target')).toBe(
|
|
189
|
+
'_blank',
|
|
190
|
+
);
|
|
191
|
+
expect(getByTitle('External blacklisted app').getAttribute('rel')).toBe(
|
|
192
|
+
'noopener noreferrer',
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
160
196
|
it('check UniversalLink does not break with error in item', () => {
|
|
161
197
|
const component = renderer.create(
|
|
162
198
|
<Provider store={store}>
|
|
@@ -111,7 +111,7 @@ export function normalizeValue(choices, value, intl) {
|
|
|
111
111
|
|
|
112
112
|
if (Array.isArray(value)) {
|
|
113
113
|
// a list of values, like ['foo', 'bar'];
|
|
114
|
-
return value.map((v) => normalizeValue(choices, v));
|
|
114
|
+
return value.map((v) => normalizeValue(choices, v, intl));
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (isObject(value)) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module components/theme/Error/ServerError
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { FormattedMessage } from 'react-intl';
|
|
7
|
+
import { Container } from 'semantic-ui-react';
|
|
8
|
+
import { withServerErrorCode } from '@plone/volto/helpers/Utils/Utils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* server error
|
|
12
|
+
* @function ServerError
|
|
13
|
+
* @returns {string} Markup of the server error page.
|
|
14
|
+
*/
|
|
15
|
+
const ServerError = () => (
|
|
16
|
+
<Container className="view-wrapper">
|
|
17
|
+
<h1>
|
|
18
|
+
<FormattedMessage id="Server Error" defaultMessage="Server Error" />
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="description">
|
|
21
|
+
<FormattedMessage
|
|
22
|
+
id="We apologize for the inconvenience, but there was an unexpected error on the server."
|
|
23
|
+
defaultMessage="We apologize for the inconvenience, but there was an unexpected error on the server."
|
|
24
|
+
/>
|
|
25
|
+
</p>
|
|
26
|
+
</Container>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export default withServerErrorCode(500)(ServerError);
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { Component } from 'react';
|
|
7
7
|
import PropTypes from 'prop-types';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Container as SemanticContainer,
|
|
10
|
+
GridColumn,
|
|
11
|
+
Segment,
|
|
12
|
+
} from 'semantic-ui-react';
|
|
9
13
|
import { Button, Modal, Grid } from 'semantic-ui-react';
|
|
10
14
|
import { Icon, UniversalLink, PreviewImage } from '@plone/volto/components';
|
|
15
|
+
import config from '@plone/volto/registry';
|
|
11
16
|
|
|
12
17
|
import openSVG from '@plone/volto/icons/open.svg';
|
|
13
18
|
import aheadSVG from '@plone/volto/icons/ahead.svg';
|
|
@@ -56,6 +61,9 @@ class AlbumView extends Component {
|
|
|
56
61
|
|
|
57
62
|
render() {
|
|
58
63
|
const { content } = this.props;
|
|
64
|
+
const Container =
|
|
65
|
+
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
66
|
+
|
|
59
67
|
return (
|
|
60
68
|
<Container className="view-wrapper">
|
|
61
69
|
<article id="content">
|
|
@@ -9,6 +9,8 @@ import { hasBlocksData, flattenHTMLToAppURL } from '@plone/volto/helpers';
|
|
|
9
9
|
import { Image, Grid } from 'semantic-ui-react';
|
|
10
10
|
import RenderBlocks from '@plone/volto/components/theme/View/RenderBlocks';
|
|
11
11
|
import { EventDetails } from '@plone/volto/components';
|
|
12
|
+
import { Container as SemanticContainer } from 'semantic-ui-react';
|
|
13
|
+
import config from '@plone/volto/registry';
|
|
12
14
|
|
|
13
15
|
const EventTextfieldView = ({ content }) => (
|
|
14
16
|
<React.Fragment>
|
|
@@ -41,9 +43,11 @@ const EventTextfieldView = ({ content }) => (
|
|
|
41
43
|
*/
|
|
42
44
|
const EventView = (props) => {
|
|
43
45
|
const { content } = props;
|
|
46
|
+
const Container =
|
|
47
|
+
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
44
48
|
|
|
45
49
|
return (
|
|
46
|
-
<
|
|
50
|
+
<Container id="page-document" className="view-wrapper event-view">
|
|
47
51
|
<Grid>
|
|
48
52
|
<Grid.Column width={7} className="mobile hidden">
|
|
49
53
|
{hasBlocksData(content) ? (
|
|
@@ -83,7 +87,7 @@ const EventView = (props) => {
|
|
|
83
87
|
)}
|
|
84
88
|
</Grid.Column>
|
|
85
89
|
</Grid>
|
|
86
|
-
</
|
|
90
|
+
</Container>
|
|
87
91
|
);
|
|
88
92
|
};
|
|
89
93
|
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import PropTypes from 'prop-types';
|
|
8
|
-
import { Container } from 'semantic-ui-react';
|
|
9
|
-
|
|
8
|
+
import { Container as SemanticContainer } from 'semantic-ui-react';
|
|
10
9
|
import { flattenToAppURL } from '@plone/volto/helpers';
|
|
10
|
+
import config from '@plone/volto/registry';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* File view component class.
|
|
@@ -15,22 +15,27 @@ import { flattenToAppURL } from '@plone/volto/helpers';
|
|
|
15
15
|
* @params {object} content Content object.
|
|
16
16
|
* @returns {string} Markup of the component.
|
|
17
17
|
*/
|
|
18
|
-
const FileView = ({ content }) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{content.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
const FileView = ({ content }) => {
|
|
19
|
+
const Container =
|
|
20
|
+
config.getComponent({ name: 'Container' }).component || SemanticContainer;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Container className="view-wrapper">
|
|
24
|
+
<h1 className="documentFirstHeading">
|
|
25
|
+
{content.title}
|
|
26
|
+
{content.subtitle && ` - ${content.subtitle}`}
|
|
27
|
+
</h1>
|
|
28
|
+
{content.description && (
|
|
29
|
+
<p className="documentDescription">{content.description}</p>
|
|
30
|
+
)}
|
|
31
|
+
{content.file?.download && (
|
|
32
|
+
<a href={flattenToAppURL(content.file.download)}>
|
|
33
|
+
{content.file.filename}
|
|
34
|
+
</a>
|
|
35
|
+
)}
|
|
36
|
+
</Container>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
41
|
* Property types.
|