@plone/volto 16.20.0 → 16.20.2
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 +4 -22
- package/.eslintrc +21 -1
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +19 -0
- package/package.json +6 -3
- package/packages/volto-slate/package.json +1 -1
- package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +0 -20
- package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +2 -1
- package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +5 -4
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +28 -19
- package/src/components/manage/Contents/Contents.jsx +29 -24
- package/src/components/manage/Widgets/ObjectListWidget.jsx +3 -8
- package/src/express-middleware/sitemap.js +36 -3
- package/src/helpers/Robots/Robots.js +24 -6
- package/src/helpers/Sitemap/Sitemap.js +44 -2
- package/src/helpers/Utils/Utils.js +4 -4
- package/src/helpers/Utils/usePagination.js +14 -48
- package/src/helpers/index.js +1 -0
- package/src/helpers/Utils/usePagination.test.js +0 -115
package/.changelog.draft
CHANGED
|
@@ -1,26 +1,8 @@
|
|
|
1
|
-
## 16.20.
|
|
2
|
-
|
|
3
|
-
### Feature
|
|
4
|
-
|
|
5
|
-
- Support RelationList field with named StaticCatalogVocabulary and SelectWidget. @ksuess [#4614](https://github.com/plone/volto/issues/4614)
|
|
6
|
-
- Support for declaring a theme in `volto.config.js` or in `package.json`
|
|
7
|
-
Add two entry points to allow extension of a theme from other add-ons. @sneridagh [#4625](https://github.com/plone/volto/issues/4625)
|
|
8
|
-
- Added querystring search get option. @robgietema [#4658](https://github.com/plone/volto/issues/4658)
|
|
1
|
+
## 16.20.2 (2023-04-18)
|
|
9
2
|
|
|
10
3
|
### Bugfix
|
|
11
4
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
### Internal
|
|
17
|
-
|
|
18
|
-
- Update to p.restapi 8.36.0 and Plone 6.0.3 @sneridagh [#4682](https://github.com/plone/volto/issues/4682)
|
|
19
|
-
|
|
20
|
-
### Documentation
|
|
21
|
-
|
|
22
|
-
- Update Volto contributing to align with and refer to the new Plone core code contributing requirements. @stevepiercy [#4634](https://github.com/plone/volto/issues/4634)
|
|
23
|
-
- Improve creating views documentation page. @rboixaderg [#4636](https://github.com/plone/volto/issues/4636)
|
|
24
|
-
- Rename "Developer Guidelines" to "Contributing". @stevepiercy [#4666](https://github.com/plone/volto/issues/4666)
|
|
25
|
-
- Fix broken link to `ReactJS.org`. @stevepiercy [#4667](https://github.com/plone/volto/issues/4667)
|
|
5
|
+
- Fix robot.txt - the sitemap link should respect x-forwarded headers @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
|
|
6
|
+
- Fix Move to top of folder ordering in folder content view by searching also @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
|
|
7
|
+
- Fix faulty D&D elements in ObjectBrowserList widget @sneridagh [#4703](https://github.com/plone/volto/issues/4703)
|
|
26
8
|
|
package/.eslintrc
CHANGED
|
@@ -44,8 +44,28 @@
|
|
|
44
44
|
"rootPathSuffix": "src"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
"import/core-modules": ["load-volto-addons"]
|
|
47
|
+
"import/core-modules": ["load-volto-addons"],
|
|
48
|
+
"react": {
|
|
49
|
+
"version": "detect"
|
|
50
|
+
}
|
|
48
51
|
},
|
|
52
|
+
"overrides": [
|
|
53
|
+
{
|
|
54
|
+
"files": ["**/*.ts", "**/*.tsx"],
|
|
55
|
+
"extends": ["plugin:@typescript-eslint/recommended", "react-app", "prettier", "plugin:jsx-a11y/recommended"],
|
|
56
|
+
"plugins": ["@typescript-eslint", "prettier", "react-hooks", "jsx-a11y"],
|
|
57
|
+
"parser": "@typescript-eslint/parser"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"files": [
|
|
61
|
+
"**/*.stories.js",
|
|
62
|
+
"**/*.stories.jsx"
|
|
63
|
+
],
|
|
64
|
+
"rules": {
|
|
65
|
+
"import/no-anonymous-default-export": "off"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
],
|
|
49
69
|
"globals": {
|
|
50
70
|
"root": true,
|
|
51
71
|
"__DEVELOPMENT__": true,
|
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 16.20.2 (2023-04-18)
|
|
12
|
+
|
|
13
|
+
### Bugfix
|
|
14
|
+
|
|
15
|
+
- Fix robot.txt - the sitemap link should respect x-forwarded headers @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
|
|
16
|
+
- Fix Move to top of folder ordering in folder content view by searching also @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
|
|
17
|
+
- Fix faulty D&D elements in ObjectBrowserList widget @sneridagh [#4703](https://github.com/plone/volto/issues/4703)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## 16.20.1 (2023-04-14)
|
|
21
|
+
|
|
22
|
+
### Bugfix
|
|
23
|
+
|
|
24
|
+
- Generate a split sitemap @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
|
|
25
|
+
- Fix Move to top of folder ordering in folder content view @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
|
|
26
|
+
- Revert "Add current page parameter to the route in the listing and search block pagination (#4159)" @sneridagh [#4695](https://github.com/plone/volto/issues/4695)
|
|
27
|
+
- Fix search block in edit mode re-queries multiple blocks with an empty search text @reebalazs [#4697](https://github.com/plone/volto/issues/4697)
|
|
28
|
+
|
|
29
|
+
|
|
11
30
|
## 16.20.0 (2023-04-12)
|
|
12
31
|
|
|
13
32
|
### Feature
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"version": "16.20.
|
|
12
|
+
"version": "16.20.2",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git@github.com:plone/volto.git"
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"stylelint": "./node_modules/.bin/stylelint 'theme/**/*.{css,less}' 'src/**/*.{css,less}'",
|
|
50
50
|
"stylelint:overrides": "./node_modules/.bin/stylelint 'theme/**/*.overrides' 'src/**/*.overrides'",
|
|
51
51
|
"stylelint:fix": "yarn stylelint --fix && yarn stylelint:overrides --fix",
|
|
52
|
-
"lint": "./node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx,json}'",
|
|
53
|
-
"lint:fix": "./node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx,json}'",
|
|
52
|
+
"lint": "./node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx,ts,tsx,json}'",
|
|
53
|
+
"lint:fix": "./node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx,ts,tsx,json}'",
|
|
54
54
|
"i18n": "rm -rf build/messages && NODE_ENV=production i18n",
|
|
55
55
|
"i18n:ci": "yarn i18n && git diff -G'^[^\"POT]' --exit-code",
|
|
56
56
|
"dry-release": "release-it --dry-run",
|
|
@@ -412,6 +412,8 @@
|
|
|
412
412
|
"@storybook/addon-essentials": "^6.3.0",
|
|
413
413
|
"@storybook/addon-links": "^6.3.0",
|
|
414
414
|
"@storybook/react": "^6.3.0",
|
|
415
|
+
"@typescript-eslint/eslint-plugin": "5.58.0",
|
|
416
|
+
"@typescript-eslint/parser": "5.58.0",
|
|
415
417
|
"babel-loader": "8.2.2",
|
|
416
418
|
"full-icu": "1.4.0",
|
|
417
419
|
"identity-obj-proxy": "3.0.0",
|
|
@@ -421,6 +423,7 @@
|
|
|
421
423
|
"react-is": "^16.13.1",
|
|
422
424
|
"release-it": "^15.1.3",
|
|
423
425
|
"tmp": "0.2.1",
|
|
426
|
+
"typescript": "5.0.4",
|
|
424
427
|
"use-trace-update": "1.3.2",
|
|
425
428
|
"why": "0.6.2"
|
|
426
429
|
},
|
|
@@ -36,26 +36,6 @@ test('renders a ListingBody component', () => {
|
|
|
36
36
|
content: {
|
|
37
37
|
data: {
|
|
38
38
|
is_folderish: true,
|
|
39
|
-
blocks: {
|
|
40
|
-
'839ee00b-013b-4f4a-9b10-8867938fdac3': {
|
|
41
|
-
'@type': 'listing',
|
|
42
|
-
block: '839ee00b-013b-4f4a-9b10-8867938fdac3',
|
|
43
|
-
headlineTag: 'h2',
|
|
44
|
-
query: [],
|
|
45
|
-
querystring: {
|
|
46
|
-
b_size: '2',
|
|
47
|
-
query: [
|
|
48
|
-
{
|
|
49
|
-
i: 'path',
|
|
50
|
-
o: 'plone.app.querystring.operation.string.absolutePath',
|
|
51
|
-
v: '/',
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
sort_order: 'ascending',
|
|
55
|
-
},
|
|
56
|
-
variation: 'default',
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
39
|
},
|
|
60
40
|
},
|
|
61
41
|
intl: {
|
|
@@ -25,7 +25,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
25
25
|
const [initialPath] = React.useState(getBaseUrl(path));
|
|
26
26
|
|
|
27
27
|
const copyFields = ['limit', 'query', 'sort_on', 'sort_order', 'depth'];
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
const adaptedQuery = Object.assign(
|
|
30
30
|
variation?.fullobjects ? { fullobjects: 1 } : { metadata_fields: '_all' },
|
|
31
31
|
{
|
|
@@ -37,6 +37,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
37
37
|
: {},
|
|
38
38
|
),
|
|
39
39
|
);
|
|
40
|
+
const { currentPage, setCurrentPage } = usePagination(querystring, 1);
|
|
40
41
|
const querystringResults = useSelector(
|
|
41
42
|
(state) => state.querystringsearch.subrequests,
|
|
42
43
|
);
|
|
@@ -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',
|
|
@@ -144,16 +148,12 @@ const getSearchFields = (searchData) => {
|
|
|
144
148
|
};
|
|
145
149
|
|
|
146
150
|
/**
|
|
147
|
-
* A
|
|
151
|
+
* A HOC that will mirror the search block state to a hash location
|
|
148
152
|
*/
|
|
149
153
|
const useHashState = () => {
|
|
150
154
|
const location = useLocation();
|
|
151
155
|
const history = useHistory();
|
|
152
156
|
|
|
153
|
-
/**
|
|
154
|
-
* Required to maintain parameter compatibility.
|
|
155
|
-
With this we will maintain support for receiving hash (#) and search (?) type parameters.
|
|
156
|
-
*/
|
|
157
157
|
const oldState = React.useMemo(() => {
|
|
158
158
|
return {
|
|
159
159
|
...qs.parse(location.search),
|
|
@@ -169,7 +169,7 @@ const useHashState = () => {
|
|
|
169
169
|
|
|
170
170
|
const setSearchData = React.useCallback(
|
|
171
171
|
(searchData) => {
|
|
172
|
-
const newParams = qs.parse(location.
|
|
172
|
+
const newParams = qs.parse(location.hash);
|
|
173
173
|
|
|
174
174
|
let changed = false;
|
|
175
175
|
|
|
@@ -186,11 +186,11 @@ const useHashState = () => {
|
|
|
186
186
|
|
|
187
187
|
if (changed) {
|
|
188
188
|
history.push({
|
|
189
|
-
|
|
189
|
+
hash: qs.stringify(newParams),
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
},
|
|
193
|
-
[history, oldState, location.
|
|
193
|
+
[history, oldState, location.hash],
|
|
194
194
|
);
|
|
195
195
|
|
|
196
196
|
return [current, setSearchData];
|
|
@@ -282,8 +282,14 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
282
282
|
const timeoutRef = React.useRef();
|
|
283
283
|
const facetSettings = data?.facets;
|
|
284
284
|
|
|
285
|
+
const deepQuery = JSON.stringify(data.query);
|
|
285
286
|
const onTriggerSearch = React.useCallback(
|
|
286
|
-
(
|
|
287
|
+
(
|
|
288
|
+
toSearchText = undefined,
|
|
289
|
+
toSearchFacets = undefined,
|
|
290
|
+
toSortOn = undefined,
|
|
291
|
+
toSortOrder = undefined,
|
|
292
|
+
) => {
|
|
287
293
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
288
294
|
timeoutRef.current = setTimeout(
|
|
289
295
|
() => {
|
|
@@ -291,7 +297,7 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
291
297
|
id,
|
|
292
298
|
query: data.query || {},
|
|
293
299
|
facets: toSearchFacets || facets,
|
|
294
|
-
searchText: toSearchText,
|
|
300
|
+
searchText: toSearchText || searchText,
|
|
295
301
|
sortOn: toSortOn || sortOn,
|
|
296
302
|
sortOrder: toSortOrder || sortOrder,
|
|
297
303
|
facetSettings,
|
|
@@ -305,11 +311,14 @@ const withSearch = (options) => (WrappedComponent) => {
|
|
|
305
311
|
toSearchFacets ? inputDelay / 3 : inputDelay,
|
|
306
312
|
);
|
|
307
313
|
},
|
|
314
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
308
315
|
[
|
|
309
|
-
data.query
|
|
316
|
+
// Use deep comparison of data.query
|
|
317
|
+
deepQuery,
|
|
310
318
|
facets,
|
|
311
319
|
id,
|
|
312
320
|
setLocationSearchData,
|
|
321
|
+
searchText,
|
|
313
322
|
sortOn,
|
|
314
323
|
sortOrder,
|
|
315
324
|
facetSettings,
|
|
@@ -796,18 +796,20 @@ class Contents extends Component {
|
|
|
796
796
|
*/
|
|
797
797
|
onMoveToTop(event, { value }) {
|
|
798
798
|
const id = this.state.items[value]['@id'];
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
799
|
+
this.props
|
|
800
|
+
.orderContent(
|
|
801
|
+
getBaseUrl(this.props.pathname),
|
|
802
|
+
id.replace(/^.*\//, ''),
|
|
803
|
+
'top',
|
|
804
|
+
)
|
|
805
|
+
.then(() => {
|
|
806
|
+
this.setState(
|
|
807
|
+
{
|
|
808
|
+
currentPage: 0,
|
|
809
|
+
},
|
|
810
|
+
() => this.fetchContents(),
|
|
811
|
+
);
|
|
812
|
+
});
|
|
811
813
|
}
|
|
812
814
|
|
|
813
815
|
/**
|
|
@@ -818,18 +820,21 @@ class Contents extends Component {
|
|
|
818
820
|
* @returns {undefined}
|
|
819
821
|
*/
|
|
820
822
|
onMoveToBottom(event, { value }) {
|
|
821
|
-
this.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
823
|
+
const id = this.state.items[value]['@id'];
|
|
824
|
+
this.props
|
|
825
|
+
.orderContent(
|
|
826
|
+
getBaseUrl(this.props.pathname),
|
|
827
|
+
id.replace(/^.*\//, ''),
|
|
828
|
+
'bottom',
|
|
829
|
+
)
|
|
830
|
+
.then(() => {
|
|
831
|
+
this.setState(
|
|
832
|
+
{
|
|
833
|
+
currentPage: 0,
|
|
834
|
+
},
|
|
835
|
+
() => this.fetchContents(),
|
|
836
|
+
);
|
|
837
|
+
});
|
|
833
838
|
}
|
|
834
839
|
|
|
835
840
|
/**
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { defineMessages, useIntl } from 'react-intl';
|
|
3
3
|
import { Accordion, Button, Segment } from 'semantic-ui-react';
|
|
4
4
|
import { DragDropList, FormFieldWrapper, Icon } from '@plone/volto/components';
|
|
5
|
-
import { applySchemaDefaults } from '@plone/volto/helpers';
|
|
5
|
+
import { applySchemaDefaults, reorderArray } from '@plone/volto/helpers';
|
|
6
6
|
import ObjectWidget from '@plone/volto/components/manage/Widgets/ObjectWidget';
|
|
7
7
|
|
|
8
8
|
import upSVG from '@plone/volto/icons/up-key.svg';
|
|
@@ -164,13 +164,8 @@ const ObjectListWidget = (props) => {
|
|
|
164
164
|
if (!destination) {
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const second = value[destination.index];
|
|
170
|
-
value[destination.index] = first;
|
|
171
|
-
value[source.index] = second;
|
|
172
|
-
|
|
173
|
-
onChange(id, value);
|
|
167
|
+
const newValue = reorderArray(value, source.index, destination.index);
|
|
168
|
+
onChange(id, newValue);
|
|
174
169
|
return true;
|
|
175
170
|
}}
|
|
176
171
|
>
|
|
@@ -1,11 +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(
|
|
28
|
+
res.set(
|
|
29
|
+
'Content-Disposition',
|
|
30
|
+
`attachment; filename="sitemap${batchStr || ''}.xml.gz"`,
|
|
31
|
+
);
|
|
9
32
|
res.send(sitemap);
|
|
10
33
|
} else {
|
|
11
34
|
// {"errno":-111, "code":"ECONNREFUSED", "host": ...}
|
|
@@ -16,10 +39,20 @@ export const sitemap = function (req, res, next) {
|
|
|
16
39
|
});
|
|
17
40
|
};
|
|
18
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
|
+
|
|
19
50
|
export default function () {
|
|
20
51
|
const middleware = express.Router();
|
|
21
52
|
|
|
22
53
|
middleware.all('**/sitemap.xml.gz', sitemap);
|
|
54
|
+
middleware.all('**/sitemap:batch.xml.gz', sitemap);
|
|
55
|
+
middleware.all('**/sitemap-index.xml', sitemapIndex);
|
|
23
56
|
middleware.id = 'sitemap.xml.gz';
|
|
24
57
|
return middleware;
|
|
25
58
|
}
|
|
@@ -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,27 @@ export const generateRobots = (req) =>
|
|
|
31
28
|
if (error) {
|
|
32
29
|
resolve(text || error);
|
|
33
30
|
} else {
|
|
31
|
+
// It appears that express does not take the x-forwarded headers into
|
|
32
|
+
// consideration, so we do it ourselves.
|
|
33
|
+
const {
|
|
34
|
+
'x-forwarded-proto': forwardedProto,
|
|
35
|
+
'x-forwarded-host': forwardedHost,
|
|
36
|
+
'x-forwarded-port': forwardedPort,
|
|
37
|
+
} = req.headers;
|
|
38
|
+
const proto = forwardedProto ?? req.protocol;
|
|
39
|
+
const host = forwardedHost ?? req.get('Host');
|
|
40
|
+
const portNum = forwardedPort ?? req.get('Port');
|
|
41
|
+
const port =
|
|
42
|
+
(proto === 'https' && '' + portNum === '443') ||
|
|
43
|
+
(proto === 'http' && '' + portNum === '80')
|
|
44
|
+
? ''
|
|
45
|
+
: `:${portNum}`;
|
|
46
|
+
// Plone has probably returned the sitemap link with the internal url.
|
|
47
|
+
// If so, let's replace it with the current one.
|
|
48
|
+
const url = `${proto}://${host}${port}`;
|
|
49
|
+
text = text.replace(internalUrl, url);
|
|
50
|
+
// Replace the sitemap with the sitemap index.
|
|
51
|
+
text = text.replace('sitemap.xml.gz', 'sitemap-index.xml');
|
|
34
52
|
resolve(text);
|
|
35
53
|
}
|
|
36
54
|
});
|
|
@@ -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
|
+
});
|
|
@@ -258,11 +258,11 @@ export const removeFromArray = (array, index) => {
|
|
|
258
258
|
};
|
|
259
259
|
|
|
260
260
|
/**
|
|
261
|
-
*
|
|
261
|
+
* Moves an item from origin to target inside an array in an immutable way
|
|
262
262
|
* @param {Array} array Array with data
|
|
263
|
-
* @param {number} origin Index of item to be
|
|
264
|
-
* @param {number} target Index of item to be
|
|
265
|
-
* @returns {Array}
|
|
263
|
+
* @param {number} origin Index of item to be moved from
|
|
264
|
+
* @param {number} target Index of item to be moved to
|
|
265
|
+
* @returns {Array} Resultant array
|
|
266
266
|
*/
|
|
267
267
|
export const reorderArray = (array, origin, target) => {
|
|
268
268
|
const result = Array.from(array);
|
|
@@ -1,59 +1,25 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { slugify } from '@plone/volto/helpers/Utils/Utils';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @function useCreatePageQueryStringKey
|
|
9
|
-
* @description A hook that creates a key with an id if there are multiple blocks with pagination.
|
|
10
|
-
* @returns {string} Example: page || page_012345678
|
|
11
|
-
*/
|
|
12
|
-
const useCreatePageQueryStringKey = (id) => {
|
|
13
|
-
const blockTypesWithPagination = ['search', 'listing'];
|
|
14
|
-
const blocks = useSelector((state) => state?.content?.data?.blocks) || [];
|
|
15
|
-
const blocksLayout =
|
|
16
|
-
useSelector((state) => state?.content?.data?.blocks_layout?.items) || [];
|
|
17
|
-
const displayedBlocks = blocksLayout?.map((item) => blocks[item]);
|
|
18
|
-
const hasMultiplePaginations =
|
|
19
|
-
displayedBlocks.filter((item) =>
|
|
20
|
-
blockTypesWithPagination.includes(item['@type']),
|
|
21
|
-
).length > 1 || false;
|
|
22
|
-
|
|
23
|
-
return hasMultiplePaginations ? slugify(`page-${id}`) : 'page';
|
|
24
|
-
};
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { isEqual } from 'lodash';
|
|
3
|
+
import { usePrevious } from './usePrevious';
|
|
4
|
+
import useDeepCompareEffect from 'use-deep-compare-effect';
|
|
25
5
|
|
|
26
6
|
/**
|
|
27
7
|
* A pagination helper that tracks the query and resets pagination in case the
|
|
28
8
|
* query changes.
|
|
29
9
|
*/
|
|
30
|
-
export const usePagination = (
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const pageQueryStringKey = useCreatePageQueryStringKey(id);
|
|
34
|
-
const pageQueryParam =
|
|
35
|
-
qs.parse(location.search)[pageQueryStringKey] || defaultPage;
|
|
36
|
-
const [currentPage, setCurrentPage] = React.useState(
|
|
37
|
-
parseInt(pageQueryParam),
|
|
38
|
-
);
|
|
39
|
-
const queryRef = useRef(qs.parse(location.search)?.query);
|
|
10
|
+
export const usePagination = (query, defaultPage = 1) => {
|
|
11
|
+
const previousQuery = usePrevious(query);
|
|
12
|
+
const [currentPage, setCurrentPage] = React.useState(defaultPage);
|
|
40
13
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
queryRef.current = qs.parse(location.search)?.query;
|
|
45
|
-
}
|
|
46
|
-
const newParams = {
|
|
47
|
-
...qs.parse(location.search),
|
|
48
|
-
[pageQueryStringKey]: currentPage,
|
|
49
|
-
};
|
|
50
|
-
history.replace({
|
|
51
|
-
search: qs.stringify(newParams),
|
|
52
|
-
});
|
|
53
|
-
}, [currentPage, defaultPage, location.search, history, pageQueryStringKey]);
|
|
14
|
+
useDeepCompareEffect(() => {
|
|
15
|
+
setCurrentPage(defaultPage);
|
|
16
|
+
}, [query, previousQuery, defaultPage]);
|
|
54
17
|
|
|
55
18
|
return {
|
|
56
|
-
currentPage
|
|
19
|
+
currentPage:
|
|
20
|
+
previousQuery && !isEqual(previousQuery, query)
|
|
21
|
+
? defaultPage
|
|
22
|
+
: currentPage,
|
|
57
23
|
setCurrentPage,
|
|
58
24
|
};
|
|
59
25
|
};
|
package/src/helpers/index.js
CHANGED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react-hooks';
|
|
2
|
-
import { usePagination } from './usePagination';
|
|
3
|
-
import * as redux from 'react-redux';
|
|
4
|
-
import routeData from 'react-router';
|
|
5
|
-
import { slugify } from '@plone/volto/helpers/Utils/Utils';
|
|
6
|
-
|
|
7
|
-
const searchBlockId = '545b33de-92cf-4747-969d-68851837b317';
|
|
8
|
-
const searchBlockId2 = '454b33de-92cf-4747-969d-68851837b713';
|
|
9
|
-
const searchBlock = {
|
|
10
|
-
'@type': 'search',
|
|
11
|
-
query: {
|
|
12
|
-
b_size: '4',
|
|
13
|
-
query: [
|
|
14
|
-
{
|
|
15
|
-
i: 'path',
|
|
16
|
-
o: 'plone.app.querystring.operation.string.relativePath',
|
|
17
|
-
v: '',
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
sort_order: 'ascending',
|
|
21
|
-
},
|
|
22
|
-
showSearchInput: true,
|
|
23
|
-
showTotalResults: true,
|
|
24
|
-
};
|
|
25
|
-
let state = {
|
|
26
|
-
content: {
|
|
27
|
-
data: {
|
|
28
|
-
blocks: {
|
|
29
|
-
[searchBlockId]: searchBlock,
|
|
30
|
-
},
|
|
31
|
-
blocks_layout: {
|
|
32
|
-
items: [searchBlockId],
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
let mockUseLocationValue = {
|
|
39
|
-
pathname: '/testroute',
|
|
40
|
-
search: '',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const setUp = (searchParam, numberOfSearches) => {
|
|
44
|
-
mockUseLocationValue.search = searchParam;
|
|
45
|
-
if (numberOfSearches > 1) {
|
|
46
|
-
state.content.data.blocks[searchBlockId2] = searchBlock;
|
|
47
|
-
state.content.data.blocks_layout.items.push(searchBlockId2);
|
|
48
|
-
}
|
|
49
|
-
return renderHook(({ id, defaultPage }) => usePagination(id, defaultPage), {
|
|
50
|
-
initialProps: {
|
|
51
|
-
id: searchBlockId,
|
|
52
|
-
defaultPage: 1,
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
describe(`Tests for usePagination, for the block ${searchBlockId}`, () => {
|
|
58
|
-
const useLocation = jest.spyOn(routeData, 'useLocation');
|
|
59
|
-
const useHistory = jest.spyOn(routeData, 'useHistory');
|
|
60
|
-
const useSelector = jest.spyOn(redux, 'useSelector');
|
|
61
|
-
beforeEach(() => {
|
|
62
|
-
useLocation.mockReturnValue(mockUseLocationValue);
|
|
63
|
-
useHistory.mockReturnValue({ replace: jest.fn() });
|
|
64
|
-
useSelector.mockImplementation((cb) => cb(state));
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('1 paginated block with id and defaultPage 1 - shoud be 1', () => {
|
|
68
|
-
const { result } = setUp();
|
|
69
|
-
expect(result.current.currentPage).toBe(1);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('1 paginated block without params - shoud be 1', () => {
|
|
73
|
-
const { result } = setUp();
|
|
74
|
-
expect(result.current.currentPage).toBe(1);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const param1 = '?page=2';
|
|
78
|
-
it(`1 paginated block with params: ${param1} - shoud be 2`, () => {
|
|
79
|
-
const { result } = setUp(param1);
|
|
80
|
-
expect(result.current.currentPage).toBe(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const param2 = `?${slugify(`page-${searchBlockId}`)}=2`;
|
|
84
|
-
it(`2 paginated blocks with current block in the params: ${param2} - shoud be 2`, () => {
|
|
85
|
-
const { result } = setUp(param2, 2);
|
|
86
|
-
expect(result.current.currentPage).toBe(2);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const param3 = `?${slugify(`page-${searchBlockId2}`)}=2`;
|
|
90
|
-
it(`2 paginated blocks with the other block in the params: ${param3} - shoud be 1`, () => {
|
|
91
|
-
const { result } = setUp(param3, 2);
|
|
92
|
-
expect(result.current.currentPage).toBe(1);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const param4 = `?${slugify(`page-${searchBlockId}`)}=2&${slugify(
|
|
96
|
-
`page-${searchBlockId2}`,
|
|
97
|
-
)}=1`;
|
|
98
|
-
it(`2 paginated blocks with both blocks in the params, current 2: ${param4} - shoud be 2`, () => {
|
|
99
|
-
const { result } = setUp(param4, 2);
|
|
100
|
-
expect(result.current.currentPage).toBe(2);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const param5 = `?${slugify(`page-${searchBlockId}`)}=1&${slugify(
|
|
104
|
-
`page-${searchBlockId2}`,
|
|
105
|
-
)}=2`;
|
|
106
|
-
it(`2 paginated blocks with both blocks in the params, current 1: ${param5} - shoud be 1`, () => {
|
|
107
|
-
const { result } = setUp(param5, 2);
|
|
108
|
-
expect(result.current.currentPage).toBe(1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it(`2 paginated blocks with wrong page param: ${param1} - shoud be 1`, () => {
|
|
112
|
-
const { result } = setUp(param1, 2);
|
|
113
|
-
expect(result.current.currentPage).toBe(1);
|
|
114
|
-
});
|
|
115
|
-
});
|