@plone/volto 18.6.0 → 18.8.0
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 +53 -0
- package/cypress/support/commands.js +22 -0
- package/locales/nl/LC_MESSAGES/volto.po +9 -9
- package/locales/nl.json +1 -1
- package/package.json +4 -4
- package/src/components/manage/Blocks/Block/BlocksForm.jsx +6 -4
- package/src/components/manage/Blocks/Block/EditBlockWrapper.jsx +1 -0
- package/src/components/manage/Controlpanels/Relations/BrokenRelations.jsx +18 -14
- package/src/components/manage/Controlpanels/Relations/Relations.jsx +48 -46
- package/src/components/manage/Toolbar/More.jsx +113 -117
- package/src/components/manage/Toolbar/More.test.jsx +0 -24
- package/src/components/manage/Widgets/ObjectBrowserWidget.jsx +15 -2
- package/src/components/manage/Widgets/RegistryImageWidget.jsx +15 -16
- package/src/components/manage/WorkingCopyToastsFactory/WorkingCopyToastsFactory.jsx +53 -56
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.jsx +23 -0
- package/src/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.jsx +135 -0
- package/src/components/theme/Image/Image.jsx +8 -1
- package/src/components/theme/View/View.jsx +2 -0
- package/src/config/ControlPanels.js +0 -1
- package/src/config/index.js +0 -1
- package/src/express-middleware/robotstxt.js +4 -5
- package/src/helpers/Api/Api.js +1 -1
- package/src/helpers/Blocks/Blocks.js +26 -4
- package/src/helpers/Blocks/Blocks.test.js +46 -0
- package/src/helpers/FormValidation/validators.ts +3 -1
- package/src/helpers/Robots/Robots.js +12 -31
- package/src/hooks/clipboard/useClipboard.js +7 -3
- package/types/components/manage/Widgets/ObjectBrowserWidget.d.ts +1 -0
- package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.d.ts +1 -0
- package/types/components/theme/AlternateHrefLangs/AlternateHrefLangs.test.d.ts +1 -0
- package/types/helpers/Api/Api.d.ts +7 -0
- package/types/helpers/Blocks/Blocks.d.ts +15 -5
- package/public/robots.txt +0 -2
|
@@ -7,7 +7,6 @@ import { defineMessages, useIntl } from 'react-intl';
|
|
|
7
7
|
import Toast from '@plone/volto/components/manage/Toast/Toast';
|
|
8
8
|
import { flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
9
9
|
import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
|
|
10
|
-
import config from '@plone/volto/registry';
|
|
11
10
|
import useDeepCompareEffect from 'use-deep-compare-effect';
|
|
12
11
|
|
|
13
12
|
const messages = defineMessages({
|
|
@@ -39,48 +38,28 @@ const WorkingCopyToastsFactory = (props) => {
|
|
|
39
38
|
};
|
|
40
39
|
|
|
41
40
|
useDeepCompareEffect(() => {
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
render: (
|
|
65
|
-
<Toast
|
|
66
|
-
info
|
|
67
|
-
title={intl.formatMessage(toastMessage, {
|
|
68
|
-
title: toastTitle,
|
|
69
|
-
})}
|
|
70
|
-
content={intl.formatMessage(messages.workingCopyCreatedBy, {
|
|
71
|
-
creator: working_copy?.creator_name,
|
|
72
|
-
date: (
|
|
73
|
-
<FormattedDate
|
|
74
|
-
date={working_copy?.created}
|
|
75
|
-
format={dateOptions}
|
|
76
|
-
/>
|
|
77
|
-
),
|
|
78
|
-
})}
|
|
79
|
-
/>
|
|
80
|
-
),
|
|
81
|
-
});
|
|
82
|
-
} else {
|
|
83
|
-
toast.info(
|
|
41
|
+
if (working_copy) {
|
|
42
|
+
let toastMessage, toastTitle;
|
|
43
|
+
if (content.working_copy_of) {
|
|
44
|
+
// I'm a working copy
|
|
45
|
+
toastMessage = messages.thisIsAWorkingCopyOf;
|
|
46
|
+
toastTitle = (
|
|
47
|
+
<Link to={flattenToAppURL(content.working_copy_of['@id'])}>
|
|
48
|
+
{content.working_copy_of?.title}
|
|
49
|
+
</Link>
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
// I'm a baseline
|
|
53
|
+
toastMessage = messages.workingCopyIs;
|
|
54
|
+
toastTitle = (
|
|
55
|
+
<Link to={flattenToAppURL(working_copy['@id'])}>
|
|
56
|
+
{working_copy?.title}
|
|
57
|
+
</Link>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (toast.isActive('workingcopyinfo')) {
|
|
61
|
+
toast.update('workingcopyinfo', {
|
|
62
|
+
render: (
|
|
84
63
|
<Toast
|
|
85
64
|
info
|
|
86
65
|
title={intl.formatMessage(toastMessage, {
|
|
@@ -95,20 +74,38 @@ const WorkingCopyToastsFactory = (props) => {
|
|
|
95
74
|
/>
|
|
96
75
|
),
|
|
97
76
|
})}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
77
|
+
/>
|
|
78
|
+
),
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
toast.info(
|
|
82
|
+
<Toast
|
|
83
|
+
info
|
|
84
|
+
title={intl.formatMessage(toastMessage, {
|
|
85
|
+
title: toastTitle,
|
|
86
|
+
})}
|
|
87
|
+
content={intl.formatMessage(messages.workingCopyCreatedBy, {
|
|
88
|
+
creator: working_copy?.creator_name,
|
|
89
|
+
date: (
|
|
90
|
+
<FormattedDate
|
|
91
|
+
date={working_copy?.created}
|
|
92
|
+
format={dateOptions}
|
|
93
|
+
/>
|
|
94
|
+
),
|
|
95
|
+
})}
|
|
96
|
+
/>,
|
|
97
|
+
{
|
|
98
|
+
toastId: 'workingcopyinfo',
|
|
99
|
+
autoClose: false,
|
|
100
|
+
closeButton: false,
|
|
101
|
+
transition: null,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
107
104
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
}
|
|
106
|
+
if (!working_copy) {
|
|
107
|
+
if (toast.isActive('workingcopyinfo')) {
|
|
108
|
+
toast.dismiss('workingcopyinfo');
|
|
112
109
|
}
|
|
113
110
|
}
|
|
114
111
|
}, [pathname, content, title, working_copy, intl, lang, dateOptions]);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
3
|
+
|
|
4
|
+
const AlternateHrefLangs = (props) => {
|
|
5
|
+
const { content } = props;
|
|
6
|
+
return (
|
|
7
|
+
<Helmet>
|
|
8
|
+
{config.settings.isMultilingual &&
|
|
9
|
+
content['@components']?.translations?.items?.map((item, key) => {
|
|
10
|
+
return (
|
|
11
|
+
<link
|
|
12
|
+
key={key}
|
|
13
|
+
rel="alternate"
|
|
14
|
+
hrefLang={item.language}
|
|
15
|
+
href={item['@id']}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
})}
|
|
19
|
+
</Helmet>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { AlternateHrefLangs };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
|
|
3
|
+
|
|
4
|
+
import renderer from 'react-test-renderer';
|
|
5
|
+
import configureStore from 'redux-mock-store';
|
|
6
|
+
import { Provider } from 'react-intl-redux';
|
|
7
|
+
import config from '@plone/volto/registry';
|
|
8
|
+
|
|
9
|
+
import { AlternateHrefLangs } from './AlternateHrefLangs';
|
|
10
|
+
|
|
11
|
+
const mockStore = configureStore();
|
|
12
|
+
|
|
13
|
+
describe('AlternateHrefLangs', () => {
|
|
14
|
+
beforeEach(() => {});
|
|
15
|
+
it('non multilingual site, renders nothing', () => {
|
|
16
|
+
config.settings.isMultilingual = false;
|
|
17
|
+
const content = {
|
|
18
|
+
'@id': '/',
|
|
19
|
+
'@components': {},
|
|
20
|
+
};
|
|
21
|
+
const store = mockStore({
|
|
22
|
+
intl: {
|
|
23
|
+
locale: 'en',
|
|
24
|
+
messages: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// We need to force the component rendering
|
|
28
|
+
// to fill the Helmet
|
|
29
|
+
renderer.create(
|
|
30
|
+
<Provider store={store}>
|
|
31
|
+
<AlternateHrefLangs content={content} />
|
|
32
|
+
</Provider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
36
|
+
expect(helmetLinks.length).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
it('multilingual site, with some translations', () => {
|
|
39
|
+
config.settings.isMultilingual = true;
|
|
40
|
+
config.settings.supportedLanguages = ['en', 'es', 'eu'];
|
|
41
|
+
|
|
42
|
+
const content = {
|
|
43
|
+
'@components': {
|
|
44
|
+
translations: {
|
|
45
|
+
items: [
|
|
46
|
+
{ '@id': '/en', language: 'en' },
|
|
47
|
+
{ '@id': '/es', language: 'es' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const store = mockStore({
|
|
54
|
+
intl: {
|
|
55
|
+
locale: 'en',
|
|
56
|
+
messages: {},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// We need to force the component rendering
|
|
61
|
+
// to fill the Helmet
|
|
62
|
+
renderer.create(
|
|
63
|
+
<Provider store={store}>
|
|
64
|
+
<>
|
|
65
|
+
<AlternateHrefLangs content={content} />
|
|
66
|
+
</>
|
|
67
|
+
</Provider>,
|
|
68
|
+
);
|
|
69
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
70
|
+
|
|
71
|
+
expect(helmetLinks.length).toBe(2);
|
|
72
|
+
|
|
73
|
+
expect(helmetLinks).toContainEqual({
|
|
74
|
+
rel: 'alternate',
|
|
75
|
+
href: '/es',
|
|
76
|
+
hrefLang: 'es',
|
|
77
|
+
});
|
|
78
|
+
expect(helmetLinks).toContainEqual({
|
|
79
|
+
rel: 'alternate',
|
|
80
|
+
href: '/en',
|
|
81
|
+
hrefLang: 'en',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
it('multilingual site, with all available translations', () => {
|
|
85
|
+
config.settings.isMultilingual = true;
|
|
86
|
+
config.settings.supportedLanguages = ['en', 'es', 'eu'];
|
|
87
|
+
const store = mockStore({
|
|
88
|
+
intl: {
|
|
89
|
+
locale: 'en',
|
|
90
|
+
messages: {},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const content = {
|
|
95
|
+
'@components': {
|
|
96
|
+
translations: {
|
|
97
|
+
items: [
|
|
98
|
+
{ '@id': '/en', language: 'en' },
|
|
99
|
+
{ '@id': '/eu', language: 'eu' },
|
|
100
|
+
{ '@id': '/es', language: 'es' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// We need to force the component rendering
|
|
107
|
+
// to fill the Helmet
|
|
108
|
+
renderer.create(
|
|
109
|
+
<Provider store={store}>
|
|
110
|
+
<AlternateHrefLangs content={content} />
|
|
111
|
+
</Provider>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const helmetLinks = Helmet.peek().linkTags;
|
|
115
|
+
|
|
116
|
+
// We expect having 3 links
|
|
117
|
+
expect(helmetLinks.length).toBe(3);
|
|
118
|
+
|
|
119
|
+
expect(helmetLinks).toContainEqual({
|
|
120
|
+
rel: 'alternate',
|
|
121
|
+
href: '/eu',
|
|
122
|
+
hrefLang: 'eu',
|
|
123
|
+
});
|
|
124
|
+
expect(helmetLinks).toContainEqual({
|
|
125
|
+
rel: 'alternate',
|
|
126
|
+
href: '/es',
|
|
127
|
+
hrefLang: 'es',
|
|
128
|
+
});
|
|
129
|
+
expect(helmetLinks).toContainEqual({
|
|
130
|
+
rel: 'alternate',
|
|
131
|
+
href: '/en',
|
|
132
|
+
hrefLang: 'en',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -54,7 +54,14 @@ export default function Image({
|
|
|
54
54
|
attrs.className = cx(className, { responsive });
|
|
55
55
|
|
|
56
56
|
if (!isSvg && image.scales && Object.keys(image.scales).length > 0) {
|
|
57
|
-
const sortedScales = Object.values(
|
|
57
|
+
const sortedScales = Object.values({
|
|
58
|
+
...image.scales,
|
|
59
|
+
original: {
|
|
60
|
+
download: `${image.download}`,
|
|
61
|
+
width: image.width,
|
|
62
|
+
height: image.height,
|
|
63
|
+
},
|
|
64
|
+
}).sort((a, b) => {
|
|
58
65
|
if (a.width > b.width) return 1;
|
|
59
66
|
else if (a.width < b.width) return -1;
|
|
60
67
|
else return 0;
|
|
@@ -21,6 +21,7 @@ import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
|
|
|
21
21
|
import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url';
|
|
22
22
|
import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content';
|
|
23
23
|
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
|
|
24
|
+
import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs';
|
|
24
25
|
|
|
25
26
|
import config from '@plone/volto/registry';
|
|
26
27
|
import SlotRenderer from '../SlotRenderer/SlotRenderer';
|
|
@@ -234,6 +235,7 @@ class View extends Component {
|
|
|
234
235
|
return (
|
|
235
236
|
<div id="view" tabIndex="-1">
|
|
236
237
|
<ContentMetadataTags content={this.props.content} />
|
|
238
|
+
<AlternateHrefLangs content={this.props.content} />
|
|
237
239
|
{/* Body class if displayName in component is set */}
|
|
238
240
|
<BodyClass
|
|
239
241
|
className={
|
package/src/config/index.js
CHANGED
|
@@ -163,7 +163,6 @@ let config = {
|
|
|
163
163
|
showSelfRegistration: false,
|
|
164
164
|
contentMetadataTagsImageField: 'image',
|
|
165
165
|
contentPropertiesSchemaEnhancer: null,
|
|
166
|
-
hasWorkingCopySupport: false,
|
|
167
166
|
maxUndoLevels: 200, // undo history size for the main form
|
|
168
167
|
addonsInfo: addonsInfo,
|
|
169
168
|
workflowMapping,
|
|
@@ -4,13 +4,12 @@ import { generateRobots } from '@plone/volto/helpers/Robots/Robots';
|
|
|
4
4
|
/*
|
|
5
5
|
robots.txt - priority order:
|
|
6
6
|
|
|
7
|
-
1)
|
|
8
|
-
2)
|
|
9
|
-
3) default: plone robots.txt
|
|
7
|
+
1) VOLTO_ROBOTSTXT var in .env
|
|
8
|
+
2) robots.txt setting in the site control panel
|
|
10
9
|
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
const
|
|
12
|
+
const siteRobots = function (req, res, next) {
|
|
14
13
|
generateRobots(req).then((robots) => {
|
|
15
14
|
res.set('Content-Type', 'text/plain');
|
|
16
15
|
res.send(robots);
|
|
@@ -27,7 +26,7 @@ export default function robotstxtMiddleware() {
|
|
|
27
26
|
if (process.env.VOLTO_ROBOTSTXT) {
|
|
28
27
|
middleware.all('**/robots.txt', envRobots);
|
|
29
28
|
} else {
|
|
30
|
-
middleware.all('**/robots.txt',
|
|
29
|
+
middleware.all('**/robots.txt', siteRobots);
|
|
31
30
|
}
|
|
32
31
|
middleware.id = 'robots.txt';
|
|
33
32
|
return middleware;
|
package/src/helpers/Api/Api.js
CHANGED
|
@@ -17,7 +17,7 @@ const methods = ['get', 'post', 'put', 'patch', 'del'];
|
|
|
17
17
|
* @param {string} path Path (or URL) to be formatted.
|
|
18
18
|
* @returns {string} Formatted path.
|
|
19
19
|
*/
|
|
20
|
-
function formatUrl(path) {
|
|
20
|
+
export function formatUrl(path) {
|
|
21
21
|
const { settings } = config;
|
|
22
22
|
const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
|
|
23
23
|
|
|
@@ -120,9 +120,10 @@ export function moveBlock(formData, source, destination) {
|
|
|
120
120
|
* @function deleteBlock
|
|
121
121
|
* @param {Object} formData Form data
|
|
122
122
|
* @param {string} blockId Block uid
|
|
123
|
+
* @param {Object} intl intl object.
|
|
123
124
|
* @return {Object} New form data
|
|
124
125
|
*/
|
|
125
|
-
export function deleteBlock(formData, blockId) {
|
|
126
|
+
export function deleteBlock(formData, blockId, intl) {
|
|
126
127
|
const blocksFieldname = getBlocksFieldname(formData);
|
|
127
128
|
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
|
|
128
129
|
|
|
@@ -135,7 +136,13 @@ export function deleteBlock(formData, blockId) {
|
|
|
135
136
|
};
|
|
136
137
|
|
|
137
138
|
if (newFormData[blocksLayoutFieldname].items.length === 0) {
|
|
138
|
-
newFormData = addBlock(
|
|
139
|
+
newFormData = addBlock(
|
|
140
|
+
newFormData,
|
|
141
|
+
config.settings.defaultBlockType,
|
|
142
|
+
0,
|
|
143
|
+
{},
|
|
144
|
+
intl,
|
|
145
|
+
);
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
return newFormData;
|
|
@@ -147,9 +154,11 @@ export function deleteBlock(formData, blockId) {
|
|
|
147
154
|
* @param {Object} formData Form data
|
|
148
155
|
* @param {string} type Block type
|
|
149
156
|
* @param {number} index Destination index
|
|
157
|
+
* @param {Object} blocksConfig Blocks configuration.
|
|
158
|
+
* @param {Object} intl intl object.
|
|
150
159
|
* @return {Array} New block id, New form data
|
|
151
160
|
*/
|
|
152
|
-
export function addBlock(formData, type, index, blocksConfig) {
|
|
161
|
+
export function addBlock(formData, type, index, blocksConfig, intl) {
|
|
153
162
|
const { settings } = config;
|
|
154
163
|
const id = uuid();
|
|
155
164
|
const idTrailingBlock = uuid();
|
|
@@ -192,6 +201,7 @@ export function addBlock(formData, type, index, blocksConfig) {
|
|
|
192
201
|
},
|
|
193
202
|
selected: id,
|
|
194
203
|
},
|
|
204
|
+
intl,
|
|
195
205
|
}),
|
|
196
206
|
];
|
|
197
207
|
}
|
|
@@ -208,6 +218,7 @@ export const applyBlockInitialValue = ({
|
|
|
208
218
|
value,
|
|
209
219
|
blocksConfig,
|
|
210
220
|
formData,
|
|
221
|
+
intl,
|
|
211
222
|
}) => {
|
|
212
223
|
const type = value['@type'];
|
|
213
224
|
blocksConfig = blocksConfig || config.blocks.blocksConfig;
|
|
@@ -217,6 +228,7 @@ export const applyBlockInitialValue = ({
|
|
|
217
228
|
id,
|
|
218
229
|
value,
|
|
219
230
|
formData,
|
|
231
|
+
intl,
|
|
220
232
|
});
|
|
221
233
|
const blocksFieldname = getBlocksFieldname(formData);
|
|
222
234
|
formData[blocksFieldname][id] = value;
|
|
@@ -231,9 +243,11 @@ export const applyBlockInitialValue = ({
|
|
|
231
243
|
* @param {Object} formData Form data
|
|
232
244
|
* @param {string} id Block uid to mutate
|
|
233
245
|
* @param {number} value Block's new value
|
|
246
|
+
* @param {Object} blocksConfig Blocks configuration.
|
|
247
|
+
* @param {Object} intl intl object.
|
|
234
248
|
* @return {Object} New form data
|
|
235
249
|
*/
|
|
236
|
-
export function mutateBlock(formData, id, value, blocksConfig) {
|
|
250
|
+
export function mutateBlock(formData, id, value, blocksConfig, intl) {
|
|
237
251
|
const { settings } = config;
|
|
238
252
|
const blocksFieldname = getBlocksFieldname(formData);
|
|
239
253
|
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
|
|
@@ -260,6 +274,7 @@ export function mutateBlock(formData, id, value, blocksConfig) {
|
|
|
260
274
|
[id]: value || null,
|
|
261
275
|
},
|
|
262
276
|
},
|
|
277
|
+
intl,
|
|
263
278
|
});
|
|
264
279
|
if (!blockHasValue(block)) {
|
|
265
280
|
return newFormData;
|
|
@@ -288,6 +303,7 @@ export function mutateBlock(formData, id, value, blocksConfig) {
|
|
|
288
303
|
],
|
|
289
304
|
},
|
|
290
305
|
},
|
|
306
|
+
intl,
|
|
291
307
|
});
|
|
292
308
|
return newFormData;
|
|
293
309
|
}
|
|
@@ -298,6 +314,10 @@ export function mutateBlock(formData, id, value, blocksConfig) {
|
|
|
298
314
|
* @param {Object} formData Form data
|
|
299
315
|
* @param {string} id Insert new block before the block with this id
|
|
300
316
|
* @param {number} value New block's value
|
|
317
|
+
* @param {Object} current Current block
|
|
318
|
+
* @param {number} offset offset position
|
|
319
|
+
* @param {Object} blocksConfig Blocks configuration.
|
|
320
|
+
* @param {Object} intl intl object.
|
|
301
321
|
* @return {Array} New block id, New form data
|
|
302
322
|
*/
|
|
303
323
|
export function insertBlock(
|
|
@@ -307,6 +327,7 @@ export function insertBlock(
|
|
|
307
327
|
current = {},
|
|
308
328
|
offset = 0,
|
|
309
329
|
blocksConfig,
|
|
330
|
+
intl,
|
|
310
331
|
) {
|
|
311
332
|
const blocksFieldname = getBlocksFieldname(formData);
|
|
312
333
|
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
|
|
@@ -340,6 +361,7 @@ export function insertBlock(
|
|
|
340
361
|
],
|
|
341
362
|
},
|
|
342
363
|
},
|
|
364
|
+
intl,
|
|
343
365
|
});
|
|
344
366
|
|
|
345
367
|
return [newBlockId, newFormData];
|
|
@@ -568,6 +568,52 @@ describe('Blocks', () => {
|
|
|
568
568
|
marker: true,
|
|
569
569
|
});
|
|
570
570
|
});
|
|
571
|
+
|
|
572
|
+
it('initialValue with intl', () => {
|
|
573
|
+
// Mock intl with formatMessage function
|
|
574
|
+
const intl = {
|
|
575
|
+
formatMessage: jest.fn(({ id }) => id),
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const messages = {
|
|
579
|
+
intl: {
|
|
580
|
+
id: 'intl',
|
|
581
|
+
defaultMessage: 'intl',
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
config.blocks.blocksConfig.text.initialValue = ({
|
|
586
|
+
id,
|
|
587
|
+
value,
|
|
588
|
+
formData,
|
|
589
|
+
intl,
|
|
590
|
+
}) => {
|
|
591
|
+
return {
|
|
592
|
+
...formData.blocks[id],
|
|
593
|
+
intl: intl.formatMessage(messages.intl),
|
|
594
|
+
};
|
|
595
|
+
};
|
|
596
|
+
const [newId, form] = addBlock(
|
|
597
|
+
{
|
|
598
|
+
blocks: { a: { value: 1 }, b: { value: 2 } },
|
|
599
|
+
blocks_layout: { items: ['a', 'b'] },
|
|
600
|
+
},
|
|
601
|
+
'text',
|
|
602
|
+
1,
|
|
603
|
+
config.blocks.blocksConfig,
|
|
604
|
+
intl,
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
delete config.blocks.blocksConfig.text.initialValue;
|
|
608
|
+
|
|
609
|
+
expect(form.blocks[newId]).toStrictEqual({
|
|
610
|
+
'@type': 'text',
|
|
611
|
+
booleanField: false,
|
|
612
|
+
description: 'Default description',
|
|
613
|
+
title: 'Default title',
|
|
614
|
+
intl: 'intl',
|
|
615
|
+
});
|
|
616
|
+
});
|
|
571
617
|
});
|
|
572
618
|
|
|
573
619
|
describe('moveBlock', () => {
|
|
@@ -171,7 +171,9 @@ export const patternValidator = ({
|
|
|
171
171
|
}
|
|
172
172
|
const regex = new RegExp(field.pattern);
|
|
173
173
|
const isValid = regex.test(value);
|
|
174
|
-
return !isValid
|
|
174
|
+
return !isValid
|
|
175
|
+
? formatMessage(messages.pattern, { pattern: field.pattern })
|
|
176
|
+
: null;
|
|
175
177
|
};
|
|
176
178
|
|
|
177
179
|
export const maxItemsValidator = ({
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @module helpers/
|
|
2
|
+
* Robots helper.
|
|
3
|
+
* @module helpers/Robots
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import superagent from 'superagent';
|
|
7
|
+
|
|
7
8
|
import config from '@plone/volto/registry';
|
|
9
|
+
import { formatUrl } from '@plone/volto/helpers/Api/Api';
|
|
8
10
|
import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -15,43 +17,22 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
|
15
17
|
*/
|
|
16
18
|
export const generateRobots = (req) =>
|
|
17
19
|
new Promise((resolve) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
config.settings.devProxyToApiPath ??
|
|
21
|
-
config.settings.apiPath;
|
|
22
|
-
const request = superagent.get(`${internalUrl}/robots.txt`);
|
|
23
|
-
request.set('Accept', 'text/plain');
|
|
20
|
+
const request = superagent.get(formatUrl('@site'));
|
|
21
|
+
request.set('Accept', 'application/json');
|
|
24
22
|
const authToken = req.universalCookies.get('auth_token');
|
|
25
23
|
if (authToken) {
|
|
26
24
|
request.set('Authorization', `Bearer ${authToken}`);
|
|
27
25
|
}
|
|
28
26
|
request.use(addHeadersFactory(req));
|
|
29
|
-
request.end((error, { text }) => {
|
|
27
|
+
request.end((error, { text, body }) => {
|
|
30
28
|
if (error) {
|
|
31
29
|
resolve(text || error);
|
|
32
30
|
} else {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'x-forwarded-port': forwardedPort,
|
|
39
|
-
} = req.headers;
|
|
40
|
-
const proto = forwardedProto ?? req.protocol;
|
|
41
|
-
const host = forwardedHost ?? req.get('Host');
|
|
42
|
-
const portNum = forwardedPort ?? req.get('Port');
|
|
43
|
-
const port =
|
|
44
|
-
(proto === 'https' && '' + portNum === '443') ||
|
|
45
|
-
(proto === 'http' && '' + portNum === '80')
|
|
46
|
-
? ''
|
|
47
|
-
: `:${portNum}`;
|
|
48
|
-
// Plone has probably returned the sitemap link with the internal url.
|
|
49
|
-
// If so, let's replace it with the current one.
|
|
50
|
-
const url = `${proto}://${host}${port}`;
|
|
51
|
-
text = text.replace(internalUrl, url);
|
|
52
|
-
// Replace the sitemap with the sitemap index.
|
|
53
|
-
text = text.replace('sitemap.xml.gz', 'sitemap-index.xml');
|
|
54
|
-
resolve(text);
|
|
31
|
+
resolve(
|
|
32
|
+
body['plone.robots_txt']
|
|
33
|
+
.replace('{portal_url}', config.settings.publicURL)
|
|
34
|
+
.replace('sitemap.xml.gz', 'sitemap-index.xml'),
|
|
35
|
+
);
|
|
55
36
|
}
|
|
56
37
|
});
|
|
57
38
|
});
|
|
@@ -13,9 +13,13 @@ export default function useClipboard(clipboardText = '') {
|
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const copyAction = useCallback(() => {
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const copyAction = useCallback(async () => {
|
|
17
|
+
try {
|
|
18
|
+
await copyToClipboard(stringToCopy.current);
|
|
19
|
+
setCopied(true);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
setCopied(false);
|
|
22
|
+
}
|
|
19
23
|
}, [stringToCopy]);
|
|
20
24
|
|
|
21
25
|
useEffect(() => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function AlternateHrefLangs(props: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|