@plone/volto 17.0.0-alpha.4 → 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 -26
- package/CHANGELOG.md +14 -0
- package/README.md +5 -8
- package/package.json +1 -1
- 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 +14 -11
- package/src/express-middleware/sitemap.js +36 -3
- package/src/helpers/Robots/Robots.js +9 -6
- package/src/helpers/Sitemap/Sitemap.js +44 -2
- package/src/helpers/Utils/usePagination.js +14 -48
- package/src/helpers/Utils/usePagination.test.js +0 -115
package/.changelog.draft
CHANGED
|
@@ -1,33 +1,13 @@
|
|
|
1
|
-
## 17.0.0-alpha.
|
|
2
|
-
|
|
3
|
-
### Feature
|
|
4
|
-
|
|
5
|
-
- DefaultView (view of fields for content types with blocks disabled): Show field name as tip on hover of label. @ksuess [#4598](https://github.com/plone/volto/issues/4598)
|
|
6
|
-
- Support RelationList field with named StaticCatalogVocabulary and SelectWidget. @ksuess [#4614](https://github.com/plone/volto/issues/4614)
|
|
7
|
-
- Support for declaring a theme in `volto.config.js` or in `package.json`
|
|
8
|
-
Add two entry points to allow extension of a theme from other add-ons. @sneridagh [#4625](https://github.com/plone/volto/issues/4625)
|
|
9
|
-
- Set sameSite in I18N_LANGUAGE cookie @sneridagh [#4627](https://github.com/plone/volto/issues/4627)
|
|
10
|
-
- Added querystring search get option. @robgietema [#4658](https://github.com/plone/volto/issues/4658)
|
|
1
|
+
## 17.0.0-alpha.5 (2023-04-14)
|
|
11
2
|
|
|
12
3
|
### Bugfix
|
|
13
4
|
|
|
14
|
-
-
|
|
15
|
-
- Fix
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
### Internal
|
|
20
|
-
|
|
21
|
-
- Trigger CI workflows to run from external pull requests. @davisagli [#4629](https://github.com/plone/volto/issues/4629)
|
|
22
|
-
- Update to p.restapi 8.36.0 and Plone 6.0.3 @sneridagh [#4682](https://github.com/plone/volto/issues/4682)
|
|
5
|
+
- Generate a split sitemap @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
|
|
6
|
+
- Fix Move to top of folder ordering in folder content view @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
|
|
7
|
+
- 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)
|
|
8
|
+
- Fix search block in edit mode re-queries multiple blocks with an empty search text @reebalazs [#4697](https://github.com/plone/volto/issues/4697)
|
|
23
9
|
|
|
24
10
|
### Documentation
|
|
25
11
|
|
|
26
|
-
-
|
|
27
|
-
- Fix documentation build, add pins @sneridagh [#4626](https://github.com/plone/volto/issues/4626)
|
|
28
|
-
- 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)
|
|
29
|
-
- Improve creating views documentation page. @rboixaderg [#4636](https://github.com/plone/volto/issues/4636)
|
|
30
|
-
- Razzle upgrade notice in upgrade guide @sneridagh [#4641](https://github.com/plone/volto/issues/4641)
|
|
31
|
-
- Rename "Developer Guidelines" to "Contributing". @stevepiercy [#4666](https://github.com/plone/volto/issues/4666)
|
|
32
|
-
- Fix broken link to `ReactJS.org`. @stevepiercy [#4667](https://github.com/plone/volto/issues/4667)
|
|
12
|
+
- Update links for 2022 Training archive. @stevepiercy [#4635](https://github.com/plone/volto/issues/4635)
|
|
33
13
|
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 17.0.0-alpha.5 (2023-04-14)
|
|
12
|
+
|
|
13
|
+
### Bugfix
|
|
14
|
+
|
|
15
|
+
- Generate a split sitemap @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
|
|
16
|
+
- Fix Move to top of folder ordering in folder content view @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
|
|
17
|
+
- 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)
|
|
18
|
+
- Fix search block in edit mode re-queries multiple blocks with an empty search text @reebalazs [#4697](https://github.com/plone/volto/issues/4697)
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- Update links for 2022 Training archive. @stevepiercy [#4635](https://github.com/plone/volto/issues/4635)
|
|
23
|
+
|
|
24
|
+
|
|
11
25
|
## 17.0.0-alpha.4 (2023-04-12)
|
|
12
26
|
|
|
13
27
|
### Feature
|
package/README.md
CHANGED
|
@@ -186,19 +186,16 @@ You can find the latest (in-progress) documentation in [https://6.dev-docs.plone
|
|
|
186
186
|
|
|
187
187
|
## Training
|
|
188
188
|
|
|
189
|
-
On the [Plone
|
|
190
|
-
Volto-dedicated open training materials, plus React and other
|
|
191
|
-
JavaScript-centered trainings.
|
|
189
|
+
On the [Plone Training website](https://training.plone.org), you'll find Volto-dedicated training materials, plus other JavaScript-centered trainings.
|
|
192
190
|
|
|
193
191
|
- [Mastering Plone 6 Development](https://training.plone.org/mastering-plone/)
|
|
194
192
|
The comprehensive training on Plone 6 with best practice tips for developers and integrators.
|
|
195
|
-
- [Volto](https://training.plone.org/5/volto/index.html)
|
|
196
|
-
A detailed training on how to create your own website using Volto frontend.
|
|
197
193
|
- [Volto Hands-On](https://training.plone.org/voltohandson/index.html)
|
|
198
194
|
- [Volto Add-ons Development](https://training.plone.org/voltoaddons/index.html)
|
|
199
|
-
- [
|
|
200
|
-
- [
|
|
201
|
-
- [
|
|
195
|
+
- [Effective Volto](https://training.plone.org/effective-volto/index.html)
|
|
196
|
+
- [Plone Deployment](https://training.plone.org/plone-deployment/index.html)
|
|
197
|
+
- [Volto](https://2022.training.plone.org/volto/index.html) (archived)
|
|
198
|
+
- [JavaScript For Plone Developers](https://2022.training.plone.org/javascript/index.html) (archived)
|
|
202
199
|
|
|
203
200
|
## Talks
|
|
204
201
|
|
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -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
|
/**
|
|
@@ -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,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
|
+
});
|
|
@@ -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
|
};
|
|
@@ -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
|
-
});
|