@plone/volto 16.21.0 → 16.21.1

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 CHANGED
@@ -1,17 +1,6 @@
1
- ## 16.21.0 (2023-06-16)
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.1 (2023-06-23)
11
2
 
12
3
  ### Bugfix
13
4
 
14
- - Do not break validation on required number field with value 0 @cekk [#4841](https://github.com/plone/volto/issues/4841)
15
- - Fix regression in horizontal scroll in contents view, add it back @sneridagh [#4872](https://github.com/plone/volto/issues/4872)
16
- - Fix and improve the `addStyling` helper @sneridagh [#4880](https://github.com/plone/volto/issues/4880)
5
+ - Added current page parameter to route in listing and search block pagination - Fix: #3868 @bipoza [#4159](https://github.com/plone/volto/issues/4159)
17
6
 
Binary file
package/CHANGELOG.md CHANGED
@@ -8,6 +8,13 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 16.21.1 (2023-06-23)
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
  ## 16.21.0 (2023-06-16)
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
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "16.21.0",
12
+ "version": "16.21.1",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "16.21.0",
3
+ "version": "16.21.1",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -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 { data = {}, properties: content, path, variation } = props;
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 { currentPage, setCurrentPage } = usePagination(querystring, 1);
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 ? !querystringResults?.[block]?.loading : true;
55
+ const hasLoaded = hasQuery ? querystringResults?.[id]?.loaded : true;
49
56
 
50
57
  const listingItems =
51
- querystring?.query?.length > 0 && querystringResults?.[block]
52
- ? querystringResults?.[block]?.items || []
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?.[block]?.total > b_size;
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[block].total / b_size)
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[block].batching?.prev
75
+ ? querystringResults[id].batching?.prev
69
76
  : null;
70
77
  const nextBatch = showAsFolderListing
71
78
  ? content.batching?.next
72
79
  : showAsQueryListing
73
- ? querystringResults[block].batching?.next
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, block, currentPage),
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
- block,
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
- block,
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?.[block]?.total}
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 HOC that will mirror the search block state to a hash location
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.hash);
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
- hash: qs.stringify(newParams),
193
+ search: qs.stringify(newParams),
190
194
  });
191
195
  }
192
196
  },
193
- [history, oldState, location.hash],
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 { isEqual } from 'lodash';
3
- import { usePrevious } from './usePrevious';
4
- import useDeepCompareEffect from 'use-deep-compare-effect';
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 = (query, defaultPage = 1) => {
11
- const previousQuery = usePrevious(query);
12
- const [currentPage, setCurrentPage] = React.useState(defaultPage);
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
- useDeepCompareEffect(() => {
15
- setCurrentPage(defaultPage);
16
- }, [query, previousQuery, defaultPage]);
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
+ });