@plone/volto 17.0.0-alpha.10 → 17.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/cypress/support/commands.js +20 -0
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +20 -0
- package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +8 -4
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +8 -4
- package/src/helpers/Utils/usePagination.js +72 -14
- package/src/helpers/Utils/usePagination.test.js +115 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 17.0.0-alpha.11 (2023-06-09)
|
|
12
|
+
|
|
13
|
+
### Bugfix
|
|
14
|
+
|
|
15
|
+
- Added current page parameter to route in listing and search block pagination - Fix: #3868 @bipoza [#4159](https://github.com/plone/volto/issues/4159)
|
|
16
|
+
|
|
17
|
+
|
|
11
18
|
## 17.0.0-alpha.10 (2023-06-09)
|
|
12
19
|
|
|
13
20
|
### Feature
|
|
@@ -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/package.json
CHANGED
|
@@ -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';
|
|
@@ -30,7 +30,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
30
30
|
const [initialPath] = React.useState(getBaseUrl(path));
|
|
31
31
|
|
|
32
32
|
const copyFields = ['limit', 'query', 'sort_on', 'sort_order', 'depth'];
|
|
33
|
-
|
|
33
|
+
const { currentPage, setCurrentPage } = usePagination(id, 1);
|
|
34
34
|
const adaptedQuery = Object.assign(
|
|
35
35
|
variation?.fullobjects ? { fullobjects: 1 } : { metadata_fields: '_all' },
|
|
36
36
|
{
|
|
@@ -42,7 +42,9 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
42
42
|
: {},
|
|
43
43
|
),
|
|
44
44
|
);
|
|
45
|
-
const
|
|
45
|
+
const adaptedQueryRef = useRef(adaptedQuery);
|
|
46
|
+
const currentPageRef = useRef(currentPage);
|
|
47
|
+
|
|
46
48
|
const querystringResults = useSelector(
|
|
47
49
|
(state) => state.querystringsearch.subrequests,
|
|
48
50
|
);
|
|
@@ -50,7 +52,7 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
50
52
|
|
|
51
53
|
const folderItems = content?.is_folderish ? content.items : [];
|
|
52
54
|
const hasQuery = querystring?.query?.length > 0;
|
|
53
|
-
const hasLoaded = hasQuery ?
|
|
55
|
+
const hasLoaded = hasQuery ? querystringResults?.[id]?.loaded : true;
|
|
54
56
|
|
|
55
57
|
const listingItems =
|
|
56
58
|
querystring?.query?.length > 0 && querystringResults?.[id]
|
|
@@ -109,6 +111,8 @@ export default function withQuerystringResults(WrappedComponent) {
|
|
|
109
111
|
} else {
|
|
110
112
|
dispatch(getContent(initialPath, null, null, currentPage));
|
|
111
113
|
}
|
|
114
|
+
adaptedQueryRef.current = adaptedQuery;
|
|
115
|
+
currentPageRef.current = currentPage;
|
|
112
116
|
}, [
|
|
113
117
|
id,
|
|
114
118
|
isImageGallery,
|
|
@@ -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];
|
|
@@ -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
|
+
});
|