@plone/volto 16.21.0 → 16.21.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 +3 -13
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +15 -0
- package/README.md +8 -11
- package/cypress/support/commands.js +20 -0
- package/docker-compose.yml +1 -1
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/razzle.config.js +7 -0
- package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +20 -0
- package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +25 -16
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +8 -4
- package/src/components/manage/Diff/DiffField.jsx +6 -6
- package/src/components/theme/FormattedDate/FormattedDate.jsx +3 -3
- package/src/helpers/Utils/Date.js +4 -2
- package/src/helpers/Utils/Date.test.js +3 -1
- package/src/helpers/Utils/usePagination.js +72 -14
- package/src/helpers/Utils/usePagination.test.js +115 -0
package/.changelog.draft
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
## 16.21.
|
|
2
|
-
|
|
3
|
-
### Feature
|
|
4
|
-
|
|
5
|
-
- Display PAS validation errors. [tschorr] [#4801](https://github.com/plone/volto/issues/4801)
|
|
6
|
-
- Allow to deselect color in ColorPickerWidget. @ksuess [#4838](https://github.com/plone/volto/issues/4838)
|
|
7
|
-
- Added a CSS identifier to the Slate style menu options. @razvanMiu [#4846](https://github.com/plone/volto/issues/4846)
|
|
8
|
-
- Use a Container from the registry in the Form component and fallback to the Semantic UI one. @sneridagh [#4849](https://github.com/plone/volto/issues/4849)
|
|
9
|
-
- Add and enforce a new config setting, `maxFileUploadSize`. @davisagli [#4868](https://github.com/plone/volto/issues/4868)
|
|
10
|
-
- Configurable Container component from registry for some key route views. @sneridagh [#4871](https://github.com/plone/volto/issues/4871)
|
|
1
|
+
## 16.21.2 (2023-06-24)
|
|
11
2
|
|
|
12
3
|
### Bugfix
|
|
13
4
|
|
|
14
|
-
-
|
|
15
|
-
- Fix
|
|
16
|
-
- Fix and improve the `addStyling` helper @sneridagh [#4880](https://github.com/plone/volto/issues/4880)
|
|
5
|
+
- Update to version 6.0.5 of Plone backend. @davisagli [#4897](https://github.com/plone/volto/issues/4897)
|
|
6
|
+
- Fix "digital envelope routines::unsupported" error when running Volto 16 in Node 17+. @davisagli [#4901](https://github.com/plone/volto/issues/4901)
|
|
17
7
|
|
package/.yarn/install-state.gz
CHANGED
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 16.21.2 (2023-06-24)
|
|
12
|
+
|
|
13
|
+
### Bugfix
|
|
14
|
+
|
|
15
|
+
- Update to version 6.0.5 of Plone backend. @davisagli [#4897](https://github.com/plone/volto/issues/4897)
|
|
16
|
+
- Fix "digital envelope routines::unsupported" error when running Volto 16 in Node 17+. @davisagli [#4901](https://github.com/plone/volto/issues/4901)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## 16.21.1 (2023-06-23)
|
|
20
|
+
|
|
21
|
+
### Bugfix
|
|
22
|
+
|
|
23
|
+
- Added current page parameter to route in listing and search block pagination - Fix: #3868 @bipoza [#4159](https://github.com/plone/volto/issues/4159)
|
|
24
|
+
|
|
25
|
+
|
|
11
26
|
## 16.21.0 (2023-06-16)
|
|
12
27
|
|
|
13
28
|
### Feature
|
package/README.md
CHANGED
|
@@ -53,20 +53,16 @@ First get all the requirements installed on your system.
|
|
|
53
53
|
|
|
54
54
|
### Prerequisites
|
|
55
55
|
|
|
56
|
-
- [Node.js LTS (16.x)](https://nodejs.org/)
|
|
56
|
+
- [Node.js LTS (16.x or 18.x)](https://nodejs.org/)
|
|
57
57
|
- [Python](https://python.org/) - See below for specific versions.
|
|
58
58
|
- [Docker](https://www.docker.com/get-started) (if using the Plone docker images)
|
|
59
59
|
|
|
60
|
-
*UPDATE 2022-10-25*: Since 2022-10-25, NodeJS 18 is in LTS state (https://github.com/nodejs/release#release-schedule). However, due to changes in internal SSL libraries, some Volto dependencies have been deprecated and need to be updated in order to continue working in NodeJS 18, mainly Webpack 4 (see: https://github.com/webpack/webpack/issues/14532#issuecomment-947525539 for further information). You can still use it, but NodeJS should be run under a special flag: `NODE_OPTIONS=--openssl-legacy-provider`. See also Volto's PR: https://github.com/plone/volto/pull/3699 for more information.
|
|
61
|
-
|
|
62
60
|
The versions of Python that are supported in Volto depend on the version of Plone that you use.
|
|
63
61
|
|
|
64
62
|
| Plone | Python | Volto |
|
|
65
63
|
|---|---|---|
|
|
66
64
|
| 5.2 | 2.7, 3.6-3.8 | 15.0 |
|
|
67
|
-
| 6.0
|
|
68
|
-
|
|
69
|
-
At the time of this writing, Volto 16 is still in alpha status, and Plone 6 is in beta status.
|
|
65
|
+
| 6.0 | 3.8-3.11 | 16.0 |
|
|
70
66
|
|
|
71
67
|
### Create a Volto project using the generator
|
|
72
68
|
|
|
@@ -90,7 +86,7 @@ cd myvoltoproject
|
|
|
90
86
|
You can bootstrap a ready Docker Plone container with all the dependencies and ready for Volto use. We recommend to use the Plone docker builds based in `pip` [plone/plone-backend](https://github.com/plone/plone-backend) image:
|
|
91
87
|
|
|
92
88
|
```shell
|
|
93
|
-
docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.
|
|
89
|
+
docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.5
|
|
94
90
|
```
|
|
95
91
|
|
|
96
92
|
or as an alternative if you have experience with Plone and you have all the
|
|
@@ -229,10 +225,11 @@ JavaScript-centered trainings.
|
|
|
229
225
|
|
|
230
226
|
## Node Support
|
|
231
227
|
|
|
228
|
+
- Node 18: Supported since Volto 16
|
|
232
229
|
- Node 16: Supported since Volto 14
|
|
233
|
-
- Node 14: Supported
|
|
234
|
-
- Node 12:
|
|
235
|
-
- Node 10:
|
|
230
|
+
- Node 14: Supported from Volto 8.8.0 - 16
|
|
231
|
+
- Node 12: No longer supported. It was supported from Volto 4 - 15
|
|
232
|
+
- Node 10: No longer supported. It was supported from Volto 1 - 12
|
|
236
233
|
|
|
237
234
|
## Browser support
|
|
238
235
|
|
|
@@ -267,7 +264,7 @@ yarn
|
|
|
267
264
|
Either using a Docker command:
|
|
268
265
|
|
|
269
266
|
```shell
|
|
270
|
-
docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.
|
|
267
|
+
docker run -it --rm --name=plone -p 8080:8080 -e SITE=Plone -e PROFILES="plone.volto:default-homepage" plone/plone-backend:6.0.5
|
|
271
268
|
```
|
|
272
269
|
|
|
273
270
|
or using the convenience makefile command:
|
|
@@ -849,3 +849,23 @@ Cypress.Commands.add('getTableSlate', (header = false) => {
|
|
|
849
849
|
);
|
|
850
850
|
return slate;
|
|
851
851
|
});
|
|
852
|
+
|
|
853
|
+
Cypress.Commands.add('configureListingWith', (contentType) => {
|
|
854
|
+
cy.get('.sidebar-container .tabs-wrapper .menu .item')
|
|
855
|
+
.contains('Block')
|
|
856
|
+
.click();
|
|
857
|
+
cy.get('.querystring-widget .fields').contains('Add criteria').click();
|
|
858
|
+
cy.get(
|
|
859
|
+
'.querystring-widget .fields:first-of-type .field:first-of-type .react-select__menu .react-select__option',
|
|
860
|
+
)
|
|
861
|
+
.contains('Type')
|
|
862
|
+
.click();
|
|
863
|
+
|
|
864
|
+
//insert Page
|
|
865
|
+
cy.get('.querystring-widget .fields:first-of-type > .field').click();
|
|
866
|
+
cy.get(
|
|
867
|
+
'.querystring-widget .fields:first-of-type > .field .react-select__menu .react-select__option',
|
|
868
|
+
)
|
|
869
|
+
.contains(contentType)
|
|
870
|
+
.click();
|
|
871
|
+
});
|
package/docker-compose.yml
CHANGED
package/package.json
CHANGED
package/razzle.config.js
CHANGED
|
@@ -22,6 +22,13 @@ const { poToJson } = require('@plone/scripts/i18n.cjs');
|
|
|
22
22
|
|
|
23
23
|
const packageJson = require(path.join(projectRootPath, 'package.json'));
|
|
24
24
|
|
|
25
|
+
// This is a workaround to support webpack 4 on Node 17+,
|
|
26
|
+
// because the MD4 hash algorithm is no longer supported by Node.
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const crypto_orig_createHash = crypto.createHash;
|
|
29
|
+
crypto.createHash = (algorithm) =>
|
|
30
|
+
crypto_orig_createHash(algorithm === 'md4' ? 'sha256' : algorithm);
|
|
31
|
+
|
|
25
32
|
const registry = new AddonConfigurationRegistry(projectRootPath);
|
|
26
33
|
|
|
27
34
|
const defaultModify = ({
|
|
@@ -36,6 +36,26 @@ 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
|
+
},
|
|
39
59
|
},
|
|
40
60
|
},
|
|
41
61
|
intl: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
2
|
import { useDispatch, useSelector } from 'react-redux';
|
|
3
3
|
import hoistNonReactStatics from 'hoist-non-react-statics';
|
|
4
4
|
import useDeepCompareEffect from 'use-deep-compare-effect';
|
|
@@ -14,18 +14,23 @@ function getDisplayName(WrappedComponent) {
|
|
|
14
14
|
|
|
15
15
|
export default function withQuerystringResults(WrappedComponent) {
|
|
16
16
|
function WithQuerystringResults(props) {
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
data = {},
|
|
19
|
+
id = data.block,
|
|
20
|
+
properties: content,
|
|
21
|
+
path,
|
|
22
|
+
variation,
|
|
23
|
+
} = props;
|
|
18
24
|
const { settings } = config;
|
|
19
25
|
const querystring = data.querystring || data; // For backwards compat with data saved before Blocks schema. Note, this is also how the Search block passes data to ListingBody
|
|
20
26
|
|
|
21
|
-
const { block } = data;
|
|
22
27
|
const { b_size = settings.defaultPageSize } = querystring; // batchsize
|
|
23
28
|
|
|
24
29
|
// save the path so it won't trigger dispatch on eager router location change
|
|
25
30
|
const [initialPath] = React.useState(getBaseUrl(path));
|
|
26
31
|
|
|
27
32
|
const copyFields = ['limit', 'query', 'sort_on', 'sort_order', 'depth'];
|
|
28
|
-
|
|
33
|
+
const { currentPage, setCurrentPage } = usePagination(id, 1);
|
|
29
34
|
const adaptedQuery = Object.assign(
|
|
30
35
|
variation?.fullobjects ? { fullobjects: 1 } : { metadata_fields: '_all' },
|
|
31
36
|
{
|
|
@@ -37,7 +42,9 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
37
42
|
: {},
|
|
38
43
|
),
|
|
39
44
|
);
|
|
40
|
-
const
|
|
45
|
+
const adaptedQueryRef = useRef(adaptedQuery);
|
|
46
|
+
const currentPageRef = useRef(currentPage);
|
|
47
|
+
|
|
41
48
|
const querystringResults = useSelector(
|
|
42
49
|
(state) => state.querystringsearch.subrequests,
|
|
43
50
|
);
|
|
@@ -45,32 +52,32 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
45
52
|
|
|
46
53
|
const folderItems = content?.is_folderish ? content.items : [];
|
|
47
54
|
const hasQuery = querystring?.query?.length > 0;
|
|
48
|
-
const hasLoaded = hasQuery ?
|
|
55
|
+
const hasLoaded = hasQuery ? querystringResults?.[id]?.loaded : true;
|
|
49
56
|
|
|
50
57
|
const listingItems =
|
|
51
|
-
querystring?.query?.length > 0 && querystringResults?.[
|
|
52
|
-
? querystringResults?.[
|
|
58
|
+
querystring?.query?.length > 0 && querystringResults?.[id]
|
|
59
|
+
? querystringResults?.[id]?.items || []
|
|
53
60
|
: folderItems;
|
|
54
61
|
|
|
55
62
|
const showAsFolderListing = !hasQuery && content?.items_total > b_size;
|
|
56
63
|
const showAsQueryListing =
|
|
57
|
-
hasQuery && querystringResults?.[
|
|
64
|
+
hasQuery && querystringResults?.[id]?.total > b_size;
|
|
58
65
|
|
|
59
66
|
const totalPages = showAsFolderListing
|
|
60
67
|
? Math.ceil(content.items_total / b_size)
|
|
61
68
|
: showAsQueryListing
|
|
62
|
-
? Math.ceil(querystringResults[
|
|
69
|
+
? Math.ceil(querystringResults[id].total / b_size)
|
|
63
70
|
: 0;
|
|
64
71
|
|
|
65
72
|
const prevBatch = showAsFolderListing
|
|
66
73
|
? content.batching?.prev
|
|
67
74
|
: showAsQueryListing
|
|
68
|
-
? querystringResults[
|
|
75
|
+
? querystringResults[id].batching?.prev
|
|
69
76
|
: null;
|
|
70
77
|
const nextBatch = showAsFolderListing
|
|
71
78
|
? content.batching?.next
|
|
72
79
|
: showAsQueryListing
|
|
73
|
-
? querystringResults[
|
|
80
|
+
? querystringResults[id].batching?.next
|
|
74
81
|
: null;
|
|
75
82
|
|
|
76
83
|
const isImageGallery =
|
|
@@ -80,7 +87,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
80
87
|
useDeepCompareEffect(() => {
|
|
81
88
|
if (hasQuery) {
|
|
82
89
|
dispatch(
|
|
83
|
-
getQueryStringResults(initialPath, adaptedQuery,
|
|
90
|
+
getQueryStringResults(initialPath, adaptedQuery, id, currentPage),
|
|
84
91
|
);
|
|
85
92
|
} else if (isImageGallery && !hasQuery) {
|
|
86
93
|
// when used as image gallery, it doesn't need a query to list children
|
|
@@ -98,14 +105,16 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
98
105
|
},
|
|
99
106
|
],
|
|
100
107
|
},
|
|
101
|
-
|
|
108
|
+
id,
|
|
102
109
|
),
|
|
103
110
|
);
|
|
104
111
|
} else {
|
|
105
112
|
dispatch(getContent(initialPath, null, null, currentPage));
|
|
106
113
|
}
|
|
114
|
+
adaptedQueryRef.current = adaptedQuery;
|
|
115
|
+
currentPageRef.current = currentPage;
|
|
107
116
|
}, [
|
|
108
|
-
|
|
117
|
+
id,
|
|
109
118
|
isImageGallery,
|
|
110
119
|
adaptedQuery,
|
|
111
120
|
hasQuery,
|
|
@@ -118,7 +127,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
118
127
|
<WrappedComponent
|
|
119
128
|
{...props}
|
|
120
129
|
onPaginationChange={(e, { activePage }) => setCurrentPage(activePage)}
|
|
121
|
-
total={querystringResults?.[
|
|
130
|
+
total={querystringResults?.[id]?.total}
|
|
122
131
|
batch_size={b_size}
|
|
123
132
|
currentPage={currentPage}
|
|
124
133
|
totalPages={totalPages}
|
|
@@ -148,12 +148,16 @@ const getSearchFields = (searchData) => {
|
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* A
|
|
151
|
+
* A hook that will mirror the search block state to a hash location
|
|
152
152
|
*/
|
|
153
153
|
const useHashState = () => {
|
|
154
154
|
const location = useLocation();
|
|
155
155
|
const history = useHistory();
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Required to maintain parameter compatibility.
|
|
159
|
+
With this we will maintain support for receiving hash (#) and search (?) type parameters.
|
|
160
|
+
*/
|
|
157
161
|
const oldState = React.useMemo(() => {
|
|
158
162
|
return {
|
|
159
163
|
...qs.parse(location.search),
|
|
@@ -169,7 +173,7 @@ const useHashState = () => {
|
|
|
169
173
|
|
|
170
174
|
const setSearchData = React.useCallback(
|
|
171
175
|
(searchData) => {
|
|
172
|
-
const newParams = qs.parse(location.
|
|
176
|
+
const newParams = qs.parse(location.search);
|
|
173
177
|
|
|
174
178
|
let changed = false;
|
|
175
179
|
|
|
@@ -186,11 +190,11 @@ const useHashState = () => {
|
|
|
186
190
|
|
|
187
191
|
if (changed) {
|
|
188
192
|
history.push({
|
|
189
|
-
|
|
193
|
+
search: qs.stringify(newParams),
|
|
190
194
|
});
|
|
191
195
|
}
|
|
192
196
|
},
|
|
193
|
-
[history, oldState, location.
|
|
197
|
+
[history, oldState, location.search],
|
|
194
198
|
);
|
|
195
199
|
|
|
196
200
|
return [current, setSearchData];
|
|
@@ -61,12 +61,12 @@ const DiffField = ({
|
|
|
61
61
|
break;
|
|
62
62
|
case 'datetime':
|
|
63
63
|
parts = diffWords(
|
|
64
|
-
new Intl.DateTimeFormat(language, readable_date_format)
|
|
65
|
-
new Date(one)
|
|
66
|
-
|
|
67
|
-
new Intl.DateTimeFormat(language, readable_date_format)
|
|
68
|
-
new Date(two)
|
|
69
|
-
|
|
64
|
+
new Intl.DateTimeFormat(language, readable_date_format)
|
|
65
|
+
.format(new Date(one))
|
|
66
|
+
.replace('\u202F', ' '),
|
|
67
|
+
new Intl.DateTimeFormat(language, readable_date_format)
|
|
68
|
+
.format(new Date(two))
|
|
69
|
+
.replace('\u202F', ' '),
|
|
70
70
|
);
|
|
71
71
|
break;
|
|
72
72
|
case 'json':
|
|
@@ -23,9 +23,9 @@ const FormattedDate = ({
|
|
|
23
23
|
<time
|
|
24
24
|
className={className}
|
|
25
25
|
dateTime={date}
|
|
26
|
-
title={new Intl.DateTimeFormat(language, long_date_format)
|
|
27
|
-
new Date(toDate(date))
|
|
28
|
-
|
|
26
|
+
title={new Intl.DateTimeFormat(language, long_date_format)
|
|
27
|
+
.format(new Date(toDate(date)))
|
|
28
|
+
.replace('\u202F', ' ')}
|
|
29
29
|
>
|
|
30
30
|
{children
|
|
31
31
|
? children(
|
|
@@ -48,7 +48,9 @@ export function formatDate({
|
|
|
48
48
|
: short_date_format;
|
|
49
49
|
|
|
50
50
|
const formatter = new Intl.DateTimeFormat(locale, format);
|
|
51
|
-
return formatToParts
|
|
51
|
+
return formatToParts
|
|
52
|
+
? formatter.formatToParts(date)
|
|
53
|
+
: formatter.format(date).replace('\u202F', ' ');
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export function formatRelativeDate({
|
|
@@ -93,5 +95,5 @@ export function formatRelativeDate({
|
|
|
93
95
|
? ''
|
|
94
96
|
: formatToParts
|
|
95
97
|
? formatter.formatToParts(v, tag)
|
|
96
|
-
: formatter.format(v, tag); // use "now" ?
|
|
98
|
+
: formatter.format(v, tag).replace('\u202F', ' '); // use "now" ?
|
|
97
99
|
}
|
|
@@ -192,6 +192,8 @@ describe('formatRelativeDate helper', () => {
|
|
|
192
192
|
it('can use alternate style narrow', () => {
|
|
193
193
|
const now = Date.now();
|
|
194
194
|
const d = new Date(now + 3 * MONTH);
|
|
195
|
-
expect(
|
|
195
|
+
expect(['in 3 mo.', 'in 3mo']).toContain(
|
|
196
|
+
formatRelativeDate({ date: d, style: 'narrow' }),
|
|
197
|
+
);
|
|
196
198
|
});
|
|
197
199
|
});
|
|
@@ -1,25 +1,83 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { useHistory, useLocation } from 'react-router-dom';
|
|
3
|
+
import qs from 'query-string';
|
|
4
|
+
import { useSelector } from 'react-redux';
|
|
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
|
+
};
|
|
25
|
+
|
|
26
|
+
const useGetBlockType = (id) => {
|
|
27
|
+
const blocks = useSelector((state) => state?.content?.data?.blocks) || [];
|
|
28
|
+
const block = blocks[id];
|
|
29
|
+
return block ? block?.['@type'] : null;
|
|
30
|
+
};
|
|
5
31
|
|
|
6
32
|
/**
|
|
7
33
|
* A pagination helper that tracks the query and resets pagination in case the
|
|
8
34
|
* query changes.
|
|
9
35
|
*/
|
|
10
|
-
export const usePagination = (
|
|
11
|
-
const
|
|
12
|
-
const
|
|
36
|
+
export const usePagination = (id = null, defaultPage = 1) => {
|
|
37
|
+
const location = useLocation();
|
|
38
|
+
const history = useHistory();
|
|
39
|
+
const pageQueryStringKey = useCreatePageQueryStringKey(id);
|
|
40
|
+
const block_type = useGetBlockType(id);
|
|
41
|
+
const pageQueryParam =
|
|
42
|
+
qs.parse(location.search)[pageQueryStringKey] || defaultPage;
|
|
43
|
+
const [currentPage, setCurrentPageState] = React.useState(
|
|
44
|
+
parseInt(pageQueryParam),
|
|
45
|
+
);
|
|
46
|
+
const setCurrentPage = (page) => {
|
|
47
|
+
setCurrentPageState(page);
|
|
48
|
+
const newParams = {
|
|
49
|
+
...qs.parse(location.search),
|
|
50
|
+
[pageQueryStringKey]: page,
|
|
51
|
+
};
|
|
52
|
+
history.push({ search: qs.stringify(newParams) });
|
|
53
|
+
};
|
|
13
54
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
55
|
+
const queryRef = useRef(qs.parse(location.search)?.query);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (
|
|
58
|
+
queryRef.current !== qs.parse(location.search)?.query &&
|
|
59
|
+
block_type === 'search'
|
|
60
|
+
) {
|
|
61
|
+
setCurrentPageState(defaultPage);
|
|
62
|
+
const newParams = {
|
|
63
|
+
...qs.parse(location.search),
|
|
64
|
+
[pageQueryStringKey]: defaultPage,
|
|
65
|
+
};
|
|
66
|
+
delete newParams[pageQueryStringKey];
|
|
67
|
+
history.replace({ search: qs.stringify(newParams) });
|
|
68
|
+
queryRef.current = qs.parse(location.search)?.query;
|
|
69
|
+
} else {
|
|
70
|
+
setCurrentPageState(
|
|
71
|
+
parseInt(
|
|
72
|
+
qs.parse(location.search)?.[pageQueryStringKey] || defaultPage,
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [location.search, block_type]);
|
|
17
78
|
|
|
18
79
|
return {
|
|
19
|
-
currentPage
|
|
20
|
-
previousQuery && !isEqual(previousQuery, query)
|
|
21
|
-
? defaultPage
|
|
22
|
-
: currentPage,
|
|
80
|
+
currentPage,
|
|
23
81
|
setCurrentPage,
|
|
24
82
|
};
|
|
25
83
|
};
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
});
|