@plone/volto 17.0.0-alpha.3 → 17.0.0-alpha.5
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 +6 -12
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +94 -2
- package/README.md +5 -8
- package/addon-registry.js +34 -0
- package/create-theme-addons-loader.js +79 -0
- package/locales/ca/LC_MESSAGES/volto.po +2 -2
- package/locales/de/LC_MESSAGES/volto.po +4 -4
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +2 -2
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +2 -2
- package/locales/eu/LC_MESSAGES/volto.po +2 -2
- package/locales/fr/LC_MESSAGES/volto.po +2 -2
- package/locales/it/LC_MESSAGES/volto.po +2 -2
- package/locales/ja/LC_MESSAGES/volto.po +2 -2
- package/locales/nl/LC_MESSAGES/volto.po +2 -2
- package/locales/pt/LC_MESSAGES/volto.po +2 -2
- package/locales/pt_BR/LC_MESSAGES/volto.po +2 -2
- package/locales/ro/LC_MESSAGES/volto.po +2 -2
- package/locales/volto.pot +3 -3
- package/locales/zh_CN/LC_MESSAGES/volto.po +2 -2
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/pyvenv.cfg +1 -1
- package/razzle.config.js +23 -0
- package/src/actions/language/language.js +1 -0
- package/src/actions/querystringsearch/querystringsearch.js +20 -14
- package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +5 -4
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +24 -11
- package/src/components/manage/Contents/Contents.jsx +14 -11
- package/src/components/manage/Widgets/SelectUtils.js +1 -1
- package/src/components/manage/Widgets/SelectWidget.jsx +1 -1
- package/src/components/theme/PasswordReset/PasswordReset.jsx +1 -1
- package/src/components/theme/PasswordReset/RequestPasswordReset.jsx +1 -1
- package/src/components/theme/View/DefaultView.jsx +1 -1
- package/src/config/Widgets.jsx +1 -0
- package/src/config/index.js +1 -0
- package/src/express-middleware/sitemap.js +36 -4
- package/src/helpers/FormValidation/FormValidation.js +10 -1
- package/src/helpers/FormValidation/FormValidation.test.js +41 -0
- package/src/helpers/Robots/Robots.js +9 -6
- package/src/helpers/Sitemap/Sitemap.js +44 -2
- package/styles/Vocab/Plone/accept.txt +2 -0
- package/styles/Vocab/Plone/reject.txt +5 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import useDeepCompareEffect from 'use-deep-compare-effect';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
3
2
|
import { defineMessages } from 'react-intl';
|
|
4
3
|
import { compose } from 'redux';
|
|
5
4
|
|
|
@@ -60,9 +59,11 @@ const SearchBlockEdit = (props) => {
|
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
const { query = {} } = data || {};
|
|
63
|
-
|
|
62
|
+
// We don't need deep compare here, as this is just json serializable data.
|
|
63
|
+
const deepQuery = JSON.stringify(query);
|
|
64
|
+
useEffect(() => {
|
|
64
65
|
onTriggerSearch();
|
|
65
|
-
}, [
|
|
66
|
+
}, [deepQuery, onTriggerSearch]);
|
|
66
67
|
|
|
67
68
|
return (
|
|
68
69
|
<>
|
|
@@ -114,15 +114,19 @@ function normalizeState({
|
|
|
114
114
|
block: id,
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
117
|
+
// Note Ideally the searchtext functionality should be restructured as being just
|
|
118
|
+
// another facet. But right now it's the same. This means that if a searchText
|
|
119
|
+
// is provided, it will override the SearchableText facet.
|
|
120
|
+
// If there is no searchText, the SearchableText in the query remains in effect.
|
|
121
|
+
// TODO eventually the searchText should be a distinct facet from SearchableText, and
|
|
122
|
+
// the two conditions could be combined, in comparison to the current state, when
|
|
123
|
+
// one overrides the other.
|
|
125
124
|
if (searchText) {
|
|
125
|
+
params.query = params.query.reduce(
|
|
126
|
+
// Remove SearchableText from query
|
|
127
|
+
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
|
|
128
|
+
[],
|
|
129
|
+
);
|
|
126
130
|
params.query.push({
|
|
127
131
|
i: 'SearchableText',
|
|
128
132
|
o: 'plone.app.querystring.operation.string.contains',
|
|
@@ -278,8 +282,14 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
278
282
|
const timeoutRef = React.useRef();
|
|
279
283
|
const facetSettings = data?.facets;
|
|
280
284
|
|
|
285
|
+
const deepQuery = JSON.stringify(data.query);
|
|
281
286
|
const onTriggerSearch = React.useCallback(
|
|
282
|
-
(
|
|
287
|
+
(
|
|
288
|
+
toSearchText = undefined,
|
|
289
|
+
toSearchFacets = undefined,
|
|
290
|
+
toSortOn = undefined,
|
|
291
|
+
toSortOrder = undefined,
|
|
292
|
+
) => {
|
|
283
293
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
284
294
|
timeoutRef.current = setTimeout(
|
|
285
295
|
() => {
|
|
@@ -287,7 +297,7 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
287
297
|
id,
|
|
288
298
|
query: data.query || {},
|
|
289
299
|
facets: toSearchFacets || facets,
|
|
290
|
-
searchText: toSearchText,
|
|
300
|
+
searchText: toSearchText || searchText,
|
|
291
301
|
sortOn: toSortOn || sortOn,
|
|
292
302
|
sortOrder: toSortOrder || sortOrder,
|
|
293
303
|
facetSettings,
|
|
@@ -301,11 +311,14 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
301
311
|
toSearchFacets ? inputDelay / 3 : inputDelay,
|
|
302
312
|
);
|
|
303
313
|
},
|
|
314
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
304
315
|
[
|
|
305
|
-
data.query
|
|
316
|
+
// Use deep comparison of data.query
|
|
317
|
+
deepQuery,
|
|
306
318
|
facets,
|
|
307
319
|
id,
|
|
308
320
|
setLocationSearchData,
|
|
321
|
+
searchText,
|
|
309
322
|
sortOn,
|
|
310
323
|
sortOrder,
|
|
311
324
|
facetSettings,
|
|
@@ -797,17 +797,20 @@ class Contents extends Component {
|
|
|
797
797
|
onMoveToTop(event, { value }) {
|
|
798
798
|
const id = this.state.items[value]['@id'];
|
|
799
799
|
value = this.state.currentPage * this.state.pageSize + value;
|
|
800
|
-
this.props
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
{
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
800
|
+
this.props
|
|
801
|
+
.orderContent(
|
|
802
|
+
getBaseUrl(this.props.pathname),
|
|
803
|
+
id.replace(/^.*\//, ''),
|
|
804
|
+
-value,
|
|
805
|
+
)
|
|
806
|
+
.then(() => {
|
|
807
|
+
this.setState(
|
|
808
|
+
{
|
|
809
|
+
currentPage: 0,
|
|
810
|
+
},
|
|
811
|
+
() => this.fetchContents(),
|
|
812
|
+
);
|
|
813
|
+
});
|
|
811
814
|
}
|
|
812
815
|
|
|
813
816
|
/**
|
|
@@ -54,7 +54,7 @@ export function normalizeSingleSelectOption(value, intl) {
|
|
|
54
54
|
throw new Error(`Unknown value type of select widget: ${value}`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const token = value.token ?? value.value ?? 'no-value';
|
|
57
|
+
const token = value.token ?? value.value ?? value.UID ?? 'no-value';
|
|
58
58
|
const label =
|
|
59
59
|
(value.title && value.title !== 'None' ? value.title : undefined) ??
|
|
60
60
|
value.label ??
|
|
@@ -202,7 +202,7 @@ class SelectWidget extends Component {
|
|
|
202
202
|
|
|
203
203
|
const isMulti = this.props.isMulti
|
|
204
204
|
? this.props.isMulti
|
|
205
|
-
: id === 'roles' || id === 'groups';
|
|
205
|
+
: id === 'roles' || id === 'groups' || this.props.type === 'array';
|
|
206
206
|
|
|
207
207
|
return (
|
|
208
208
|
<FormFieldWrapper {...this.props}>
|
package/src/config/Widgets.jsx
CHANGED
|
@@ -98,6 +98,7 @@ export const widgetMapping = {
|
|
|
98
98
|
select_querystring_field: SelectMetadataWidget,
|
|
99
99
|
autocomplete: SelectAutoComplete,
|
|
100
100
|
color_picker: ColorPickerWidget,
|
|
101
|
+
select: SelectWidget,
|
|
101
102
|
},
|
|
102
103
|
vocabulary: {
|
|
103
104
|
'plone.app.vocabularies.Catalog': ObjectBrowserWidget,
|
package/src/config/index.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
generateSitemap,
|
|
4
|
+
generateSitemapIndex,
|
|
5
|
+
SITEMAP_BATCH_SIZE,
|
|
6
|
+
} from '@plone/volto/helpers/Sitemap/Sitemap';
|
|
3
7
|
|
|
4
8
|
export const sitemap = function (req, res, next) {
|
|
5
|
-
|
|
9
|
+
let start = 0;
|
|
10
|
+
let size = undefined;
|
|
11
|
+
const { batch: batchStr } = req.params;
|
|
12
|
+
if (batchStr !== undefined) {
|
|
13
|
+
const batch = parseInt(batchStr);
|
|
14
|
+
if (isNaN(batch) || batch === 0 || '' + batch !== batchStr) {
|
|
15
|
+
res.status(404);
|
|
16
|
+
// Some data, such as the internal API address, may be sensitive to be published
|
|
17
|
+
res.send(
|
|
18
|
+
`Invalid sitemap name, use sitemap.xml.gz, or batched sitemapN.xml.gz where N is a positive integer.`,
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
start = SITEMAP_BATCH_SIZE * (batch - 1);
|
|
23
|
+
size = SITEMAP_BATCH_SIZE;
|
|
24
|
+
}
|
|
25
|
+
generateSitemap(req, start, size).then((sitemap) => {
|
|
6
26
|
if (Buffer.isBuffer(sitemap)) {
|
|
7
27
|
res.set('Content-Type', 'application/x-gzip');
|
|
8
|
-
res.set(
|
|
9
|
-
|
|
28
|
+
res.set(
|
|
29
|
+
'Content-Disposition',
|
|
30
|
+
`attachment; filename="sitemap${batchStr || ''}.xml.gz"`,
|
|
31
|
+
);
|
|
10
32
|
res.send(sitemap);
|
|
11
33
|
} else {
|
|
12
34
|
// {"errno":-111, "code":"ECONNREFUSED", "host": ...}
|
|
@@ -17,10 +39,20 @@ export const sitemap = function (req, res, next) {
|
|
|
17
39
|
});
|
|
18
40
|
};
|
|
19
41
|
|
|
42
|
+
export const sitemapIndex = function (req, res, next) {
|
|
43
|
+
generateSitemapIndex(req).then((sitemapIndex) => {
|
|
44
|
+
res.set('Content-Type', 'application/xml');
|
|
45
|
+
res.set('Content-Disposition', 'attachment; filename="sitemap-index.xml"');
|
|
46
|
+
res.send(sitemapIndex);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
20
50
|
export default function () {
|
|
21
51
|
const middleware = express.Router();
|
|
22
52
|
|
|
23
53
|
middleware.all('**/sitemap.xml.gz', sitemap);
|
|
54
|
+
middleware.all('**/sitemap:batch.xml.gz', sitemap);
|
|
55
|
+
middleware.all('**/sitemap-index.xml', sitemapIndex);
|
|
24
56
|
middleware.id = 'sitemap.xml.gz';
|
|
25
57
|
return middleware;
|
|
26
58
|
}
|
|
@@ -65,7 +65,16 @@ const widgetValidation = {
|
|
|
65
65
|
},
|
|
66
66
|
url: {
|
|
67
67
|
isValidURL: (urlValue, urlObj, intlFunc) => {
|
|
68
|
-
|
|
68
|
+
var urlRegex = new RegExp(
|
|
69
|
+
'^(https?:\\/\\/)?' + // validate protocol
|
|
70
|
+
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name
|
|
71
|
+
'((\\d{1,3}\\.){3}\\d{1,3}))|' + // validate OR ip (v4) address
|
|
72
|
+
'(localhost)' + // validate OR localhost address
|
|
73
|
+
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path
|
|
74
|
+
'(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string
|
|
75
|
+
'(\\#[-a-z\\d_]*)?$', // validate fragment locator
|
|
76
|
+
'i',
|
|
77
|
+
);
|
|
69
78
|
const isValid = urlRegex.test(urlValue);
|
|
70
79
|
return !isValid ? intlFunc(messages.isValidURL) : null;
|
|
71
80
|
},
|
|
@@ -5,6 +5,7 @@ const schema = {
|
|
|
5
5
|
properties: {
|
|
6
6
|
username: { title: 'Username', type: 'string', description: '' },
|
|
7
7
|
email: { title: 'Email', type: 'string', widget: 'email', description: '' },
|
|
8
|
+
url: { title: 'url', type: 'string', widget: 'url', description: '' },
|
|
8
9
|
},
|
|
9
10
|
fieldsets: [
|
|
10
11
|
{ id: 'default', title: 'FIXME: User Data', fields: ['username'] },
|
|
@@ -87,5 +88,45 @@ describe('FormValidation', () => {
|
|
|
87
88
|
}),
|
|
88
89
|
).toEqual({});
|
|
89
90
|
});
|
|
91
|
+
it('validates incorrect url', () => {
|
|
92
|
+
formData.url = 'foo';
|
|
93
|
+
expect(
|
|
94
|
+
FormValidation.validateFieldsPerFieldset({
|
|
95
|
+
schema,
|
|
96
|
+
formData,
|
|
97
|
+
formatMessage,
|
|
98
|
+
}),
|
|
99
|
+
).toEqual({ url: [messages.isValidURL.defaultMessage] });
|
|
100
|
+
});
|
|
101
|
+
it('validates url', () => {
|
|
102
|
+
formData.url = 'https://plone.org/';
|
|
103
|
+
expect(
|
|
104
|
+
FormValidation.validateFieldsPerFieldset({
|
|
105
|
+
schema,
|
|
106
|
+
formData,
|
|
107
|
+
formatMessage,
|
|
108
|
+
}),
|
|
109
|
+
).toEqual({});
|
|
110
|
+
});
|
|
111
|
+
it('validates url with ip', () => {
|
|
112
|
+
formData.url = 'http://127.0.0.1:8080/Plone';
|
|
113
|
+
expect(
|
|
114
|
+
FormValidation.validateFieldsPerFieldset({
|
|
115
|
+
schema,
|
|
116
|
+
formData,
|
|
117
|
+
formatMessage,
|
|
118
|
+
}),
|
|
119
|
+
).toEqual({});
|
|
120
|
+
});
|
|
121
|
+
it('validates url with localhost', () => {
|
|
122
|
+
formData.url = 'http://localhost:8080/Plone';
|
|
123
|
+
expect(
|
|
124
|
+
FormValidation.validateFieldsPerFieldset({
|
|
125
|
+
schema,
|
|
126
|
+
formData,
|
|
127
|
+
formatMessage,
|
|
128
|
+
}),
|
|
129
|
+
).toEqual({});
|
|
130
|
+
});
|
|
90
131
|
});
|
|
91
132
|
});
|
|
@@ -15,12 +15,9 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
|
15
15
|
*/
|
|
16
16
|
export const generateRobots = (req) =>
|
|
17
17
|
new Promise((resolve) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
config.settings.internalApiPath ?? config.settings.apiPath
|
|
22
|
-
}/robots.txt`,
|
|
23
|
-
);
|
|
18
|
+
const internalUrl =
|
|
19
|
+
config.settings.internalApiPath ?? config.settings.apiPath;
|
|
20
|
+
const request = superagent.get(`${internalUrl}/robots.txt`);
|
|
24
21
|
request.set('Accept', 'text/plain');
|
|
25
22
|
const authToken = req.universalCookies.get('auth_token');
|
|
26
23
|
if (authToken) {
|
|
@@ -31,6 +28,12 @@ export const generateRobots = (req) =>
|
|
|
31
28
|
if (error) {
|
|
32
29
|
resolve(text || error);
|
|
33
30
|
} else {
|
|
31
|
+
// Plone has probably returned the sitemap link with the internal url.
|
|
32
|
+
// If so, let's replace it with the current one.
|
|
33
|
+
const url = `${req.protocol}://${req.get('Host')}`;
|
|
34
|
+
text = text.replace(internalUrl, url);
|
|
35
|
+
// Replace the sitemap with the sitemap index.
|
|
36
|
+
text = text.replace('sitemap.xml.gz', 'sitemap-index.xml');
|
|
34
37
|
resolve(text);
|
|
35
38
|
}
|
|
36
39
|
});
|
|
@@ -11,19 +11,23 @@ import { addHeadersFactory } from '@plone/volto/helpers/Proxy/Proxy';
|
|
|
11
11
|
|
|
12
12
|
import config from '@plone/volto/registry';
|
|
13
13
|
|
|
14
|
+
export const SITEMAP_BATCH_SIZE = 5000;
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* Generate sitemap
|
|
16
18
|
* @function generateSitemap
|
|
17
19
|
* @param {Object} _req Request object
|
|
18
20
|
* @return {string} Generated sitemap
|
|
19
21
|
*/
|
|
20
|
-
export const generateSitemap = (_req) =>
|
|
22
|
+
export const generateSitemap = (_req, start = 0, size = undefined) =>
|
|
21
23
|
new Promise((resolve) => {
|
|
22
24
|
const { settings } = config;
|
|
23
25
|
const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
|
|
24
26
|
const apiPath = settings.internalApiPath ?? settings.apiPath;
|
|
25
27
|
const request = superagent.get(
|
|
26
|
-
`${apiPath}${APISUFIX}/@search?metadata_fields=modified&b_size
|
|
28
|
+
`${apiPath}${APISUFIX}/@search?metadata_fields=modified&b_start=${start}&b_size=${
|
|
29
|
+
size !== undefined ? size : 100000000
|
|
30
|
+
}&use_site_search_settings=1`,
|
|
27
31
|
);
|
|
28
32
|
request.set('Accept', 'application/json');
|
|
29
33
|
request.use(addHeadersFactory(_req));
|
|
@@ -50,3 +54,41 @@ export const generateSitemap = (_req) =>
|
|
|
50
54
|
}
|
|
51
55
|
});
|
|
52
56
|
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate sitemap
|
|
60
|
+
* @function generateSitemapIndex
|
|
61
|
+
* @param {Object} _req Request object
|
|
62
|
+
* @return {string} Generated sitemap index
|
|
63
|
+
*/
|
|
64
|
+
export const generateSitemapIndex = (_req) =>
|
|
65
|
+
new Promise((resolve) => {
|
|
66
|
+
const { settings } = config;
|
|
67
|
+
const APISUFIX = settings.legacyTraverse ? '' : '/++api++';
|
|
68
|
+
const apiPath = settings.internalApiPath ?? settings.apiPath;
|
|
69
|
+
const request = superagent.get(
|
|
70
|
+
`${apiPath}${APISUFIX}/@search?metadata_fields=modified&b_size=0&use_site_search_settings=1`,
|
|
71
|
+
);
|
|
72
|
+
request.set('Accept', 'application/json');
|
|
73
|
+
const authToken = _req.universalCookies.get('auth_token');
|
|
74
|
+
if (authToken) {
|
|
75
|
+
request.set('Authorization', `Bearer ${authToken}`);
|
|
76
|
+
}
|
|
77
|
+
request.end((error, { body } = {}) => {
|
|
78
|
+
if (error) {
|
|
79
|
+
resolve(body || error);
|
|
80
|
+
} else {
|
|
81
|
+
const items = Array.from(
|
|
82
|
+
{ length: Math.ceil(body.items_total / SITEMAP_BATCH_SIZE) },
|
|
83
|
+
(_, i) =>
|
|
84
|
+
` <sitemap>
|
|
85
|
+
<loc>${toPublicURL('/sitemap' + (i + 1) + '.xml.gz')}</loc>
|
|
86
|
+
</sitemap>`,
|
|
87
|
+
);
|
|
88
|
+
const result = `<?xml version="1.0" encoding="UTF-8"?>
|
|
89
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
90
|
+
${items.join('\n')}\n</sitemapindex>`;
|
|
91
|
+
resolve(result);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|