@plone/volto 17.0.0-alpha.2 → 17.0.0-alpha.4

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.
Files changed (51) hide show
  1. package/.changelog.draft +23 -21
  2. package/.vale.ini +10 -0
  3. package/.yarn/install-state.gz +0 -0
  4. package/CHANGELOG.md +130 -2
  5. package/addon-registry.js +34 -0
  6. package/create-theme-addons-loader.js +79 -0
  7. package/locales/ca/LC_MESSAGES/volto.po +3 -3
  8. package/locales/de/LC_MESSAGES/volto.po +5 -5
  9. package/locales/de.json +1 -1
  10. package/locales/en/LC_MESSAGES/volto.po +3 -3
  11. package/locales/en.json +1 -1
  12. package/locales/es/LC_MESSAGES/volto.po +3 -3
  13. package/locales/eu/LC_MESSAGES/volto.po +3 -3
  14. package/locales/fr/LC_MESSAGES/volto.po +3 -3
  15. package/locales/it/LC_MESSAGES/volto.po +3 -3
  16. package/locales/ja/LC_MESSAGES/volto.po +3 -3
  17. package/locales/nl/LC_MESSAGES/volto.po +3 -3
  18. package/locales/pt/LC_MESSAGES/volto.po +3 -3
  19. package/locales/pt_BR/LC_MESSAGES/volto.po +3 -3
  20. package/locales/ro/LC_MESSAGES/volto.po +3 -3
  21. package/locales/volto.pot +4 -4
  22. package/locales/zh_CN/LC_MESSAGES/volto.po +3 -3
  23. package/package.json +1 -1
  24. package/packages/volto-slate/package.json +1 -1
  25. package/pyvenv.cfg +1 -1
  26. package/razzle.config.js +23 -0
  27. package/src/actions/language/language.js +1 -0
  28. package/src/actions/querystringsearch/querystringsearch.js +20 -14
  29. package/src/components/manage/BlockChooser/BlockChooser.jsx +6 -2
  30. package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +20 -0
  31. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +1 -2
  32. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +8 -4
  33. package/src/components/manage/Controlpanels/AddonsControlpanel.jsx +3 -3
  34. package/src/components/manage/Widgets/SelectUtils.js +1 -1
  35. package/src/components/manage/Widgets/SelectWidget.jsx +1 -1
  36. package/src/components/theme/PasswordReset/PasswordReset.jsx +1 -1
  37. package/src/components/theme/PasswordReset/RequestPasswordReset.jsx +1 -1
  38. package/src/components/theme/View/DefaultView.jsx +1 -1
  39. package/src/config/Widgets.jsx +1 -0
  40. package/src/config/index.js +1 -0
  41. package/src/express-middleware/sitemap.js +0 -1
  42. package/src/helpers/FormValidation/FormValidation.js +10 -1
  43. package/src/helpers/FormValidation/FormValidation.test.js +41 -0
  44. package/src/helpers/Url/Url.js +19 -3
  45. package/src/helpers/Url/Url.test.js +12 -0
  46. package/src/helpers/Utils/usePagination.js +48 -14
  47. package/src/helpers/Utils/usePagination.test.js +115 -0
  48. package/styles/Vocab/Base/accept.txt +0 -0
  49. package/styles/Vocab/Base/reject.txt +0 -0
  50. package/styles/Vocab/Plone/accept.txt +10 -0
  51. package/styles/Vocab/Plone/reject.txt +5 -0
package/razzle.config.js CHANGED
@@ -8,6 +8,7 @@ const fs = require('fs');
8
8
  const RootResolverPlugin = require('./webpack-plugins/webpack-root-resolver');
9
9
  const RelativeResolverPlugin = require('./webpack-plugins/webpack-relative-resolver');
10
10
  const createAddonsLoader = require('./create-addons-loader');
11
+ const createThemeAddonsLoader = require('./create-theme-addons-loader');
11
12
  const AddonConfigurationRegistry = require('./addon-registry');
12
13
  const CircularDependencyPlugin = require('circular-dependency-plugin');
13
14
  const TerserPlugin = require('terser-webpack-plugin');
@@ -245,6 +246,28 @@ const defaultModify = ({
245
246
  'lodash-es': path.dirname(require.resolve('lodash')),
246
247
  };
247
248
 
249
+ const [
250
+ addonsThemeLoaderVariablesPath,
251
+ addonsThemeLoaderMainPath,
252
+ ] = createThemeAddonsLoader(registry.getCustomThemeAddons());
253
+
254
+ // Automatic Theme Loading
255
+ if (registry.theme) {
256
+ // The themes should be located in `src/theme`
257
+ const themePath = registry.packages[registry.theme].modulePath;
258
+ const themeConfigPath = `${themePath}/theme/theme.config`;
259
+ config.resolve.alias['../../theme.config$'] = themeConfigPath;
260
+ config.resolve.alias['../../theme.config'] = themeConfigPath;
261
+
262
+ // We create an alias for each custom theme insertion point (variables, main)
263
+ config.resolve.alias[
264
+ 'addonsThemeCustomizationsVariables'
265
+ ] = addonsThemeLoaderVariablesPath;
266
+ config.resolve.alias[
267
+ 'addonsThemeCustomizationsMain'
268
+ ] = addonsThemeLoaderMainPath;
269
+ }
270
+
248
271
  config.performance = {
249
272
  maxAssetSize: 10000000,
250
273
  maxEntrypointSize: 10000000,
@@ -7,6 +7,7 @@ export function changeLanguageCookies(language, req) {
7
7
 
8
8
  const cookieOptions = getCookieOptions({
9
9
  secure: req?.protocol?.startsWith('https') ? true : false,
10
+ sameSite: 'strict',
10
11
  });
11
12
 
12
13
  if (!req) {
@@ -31,24 +31,30 @@ export function getQueryStringResults(path, data, subrequest, page) {
31
31
  }
32
32
  }
33
33
 
34
+ const query = {
35
+ ...requestData,
36
+ ...(!requestData.b_size && {
37
+ b_size: settings.defaultPageSize,
38
+ }),
39
+ ...(page && {
40
+ b_start: requestData.b_size
41
+ ? data.b_size * (page - 1)
42
+ : settings.defaultPageSize * (page - 1),
43
+ }),
44
+ query: requestData?.query,
45
+ };
46
+
34
47
  return {
35
48
  type: GET_QUERYSTRING_RESULTS,
36
49
  subrequest,
37
50
  request: {
38
- op: 'post',
39
- path: `${path}/@querystring-search`,
40
- data: {
41
- ...requestData,
42
- ...(!requestData.b_size && {
43
- b_size: settings.defaultPageSize,
44
- }),
45
- ...(page && {
46
- b_start: requestData.b_size
47
- ? data.b_size * (page - 1)
48
- : settings.defaultPageSize * (page - 1),
49
- }),
50
- query: requestData?.query,
51
- },
51
+ op: settings.querystringSearchGet ? 'get' : 'post',
52
+ path: `${path}/@querystring-search${
53
+ settings.querystringSearchGet
54
+ ? `?query=${encodeURIComponent(JSON.stringify(query))}`
55
+ : ''
56
+ }`,
57
+ data: settings.querystringSearchGet ? null : query,
52
58
  },
53
59
  };
54
60
  }
@@ -89,14 +89,18 @@ const BlockChooser = ({
89
89
  function blocksAvailableFilter(blocks) {
90
90
  return blocks.filter(
91
91
  (block) =>
92
- getFormatMessage(block.title).toLowerCase().includes(filterValue) ||
92
+ getFormatMessage(block.title)
93
+ .toLowerCase()
94
+ .includes(filterValue.toLowerCase()) ||
93
95
  filterVariations(block)?.length,
94
96
  );
95
97
  }
96
98
  function filterVariations(block) {
97
99
  return block.variations?.filter(
98
100
  (variation) =>
99
- getFormatMessage(variation.title).toLowerCase().includes(filterValue) &&
101
+ getFormatMessage(variation.title)
102
+ .toLowerCase()
103
+ .includes(filterValue.toLowerCase()) &&
100
104
  !variation.title.toLowerCase().includes('default'),
101
105
  );
102
106
  }
@@ -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: {
@@ -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
+ const { currentPage, setCurrentPage } = usePagination(data.block, 1);
29
29
  const adaptedQuery = Object.assign(
30
30
  variation?.fullobjects ? { fullobjects: 1 } : { metadata_fields: '_all' },
31
31
  {
@@ -37,7 +37,6 @@ export default function withQuerystringResults(WrappedComponent) {
37
37
  : {},
38
38
  ),
39
39
  );
40
- const { currentPage, setCurrentPage } = usePagination(querystring, 1);
41
40
  const querystringResults = useSelector(
42
41
  (state) => state.querystringsearch.subrequests,
43
42
  );
@@ -144,12 +144,16 @@ const getSearchFields = (searchData) => {
144
144
  };
145
145
 
146
146
  /**
147
- * A HOC that will mirror the search block state to a hash location
147
+ * A hook that will mirror the search block state to a hash location
148
148
  */
149
149
  const useHashState = () => {
150
150
  const location = useLocation();
151
151
  const history = useHistory();
152
152
 
153
+ /**
154
+ * Required to maintain parameter compatibility.
155
+ With this we will maintain support for receiving hash (#) and search (?) type parameters.
156
+ */
153
157
  const oldState = React.useMemo(() => {
154
158
  return {
155
159
  ...qs.parse(location.search),
@@ -165,7 +169,7 @@ const useHashState = () => {
165
169
 
166
170
  const setSearchData = React.useCallback(
167
171
  (searchData) => {
168
- const newParams = qs.parse(location.hash);
172
+ const newParams = qs.parse(location.search);
169
173
 
170
174
  let changed = false;
171
175
 
@@ -182,11 +186,11 @@ const useHashState = () => {
182
186
 
183
187
  if (changed) {
184
188
  history.push({
185
- hash: qs.stringify(newParams),
189
+ search: qs.stringify(newParams),
186
190
  });
187
191
  }
188
192
  },
189
- [history, oldState, location.hash],
193
+ [history, oldState, location.search],
190
194
  );
191
195
 
192
196
  return [current, setSearchData];
@@ -43,7 +43,7 @@ const messages = defineMessages({
43
43
  addAddons: {
44
44
  id: 'Add Addons',
45
45
  defaultMessage:
46
- 'To make new add-ons show up here, add them to your buildout configuration, run buildout, and restart the server process. For detailed instructions see',
46
+ 'To make new add-ons show up here, add them to your configuration, build, and restart the server process. For detailed instructions see',
47
47
  },
48
48
  addonsSettings: {
49
49
  id: 'Add-ons Settings',
@@ -380,11 +380,11 @@ class AddonsControlpanel extends Component {
380
380
  </Header>
381
381
  <FormattedMessage
382
382
  id="Add Addons"
383
- defaultMessage="To make new add-ons show up here, add them to your buildout configuration, run buildout, and restart the server process. For detailed instructions see"
383
+ defaultMessage="To make new add-ons show up here, add them to your configuration, build, and restart the server process. For detailed instructions see"
384
384
  />
385
385
  &nbsp;
386
386
  <a
387
- href="http://docs.plone.org/manage/installing/installing_addons.html"
387
+ href="https://6.docs.plone.org/install/"
388
388
  target="_blank"
389
389
  rel="noopener noreferrer"
390
390
  >
@@ -54,7 +54,7 @@ export function normalizeSingleSelectOption(value, intl) {
54
54
  throw new Error(`Unknown value type of select widget: ${value}`);
55
55
  }
56
56
 
57
- const token = value.token ?? value.value ?? 'no-value';
57
+ const token = value.token ?? value.value ?? value.UID ?? 'no-value';
58
58
  const label =
59
59
  (value.title && value.title !== 'None' ? value.title : undefined) ??
60
60
  value.label ??
@@ -202,7 +202,7 @@ class SelectWidget extends Component {
202
202
 
203
203
  const isMulti = this.props.isMulti
204
204
  ? this.props.isMulti
205
- : id === 'roles' || id === 'groups';
205
+ : id === 'roles' || id === 'groups' || this.props.type === 'array';
206
206
 
207
207
  return (
208
208
  <FormFieldWrapper {...this.props}>
@@ -31,7 +31,7 @@ const messages = defineMessages({
31
31
  },
32
32
  usernameTitle: {
33
33
  id: 'My username is',
34
- defaultMessage: 'My username is',
34
+ defaultMessage: 'My user name is',
35
35
  },
36
36
  emailTitle: {
37
37
  id: 'My email is',
@@ -32,7 +32,7 @@ const messages = defineMessages({
32
32
  },
33
33
  usernameTitle: {
34
34
  id: 'label_my_username_is',
35
- defaultMessage: 'My username is',
35
+ defaultMessage: 'My user name is',
36
36
  },
37
37
  emailTitle: {
38
38
  id: 'label_my_email_is',
@@ -87,7 +87,7 @@ const DefaultView = (props) => {
87
87
  return f !== 'title' ? (
88
88
  <Grid celled="internally" key={key}>
89
89
  <Grid.Row>
90
- <Label>{field.title}:</Label>
90
+ <Label title={field.id}>{field.title}:</Label>
91
91
  </Grid.Row>
92
92
  <Grid.Row>
93
93
  <Segment basic>
@@ -98,6 +98,7 @@ export const widgetMapping = {
98
98
  select_querystring_field: SelectMetadataWidget,
99
99
  autocomplete: SelectAutoComplete,
100
100
  color_picker: ColorPickerWidget,
101
+ select: SelectWidget,
101
102
  },
102
103
  vocabulary: {
103
104
  'plone.app.vocabularies.Catalog': ObjectBrowserWidget,
@@ -180,6 +180,7 @@ let config = {
180
180
  styleClassNameConverters,
181
181
  hashLinkSmoothScroll: false,
182
182
  styleClassNameExtenders,
183
+ querystringSearchGet: false,
183
184
  },
184
185
  experimental: {
185
186
  addBlockButton: {
@@ -5,7 +5,6 @@ export const sitemap = function (req, res, next) {
5
5
  generateSitemap(req).then((sitemap) => {
6
6
  if (Buffer.isBuffer(sitemap)) {
7
7
  res.set('Content-Type', 'application/x-gzip');
8
- res.set('Content-Encoding', 'gzip');
9
8
  res.set('Content-Disposition', 'attachment; filename="sitemap.xml.gz"');
10
9
  res.send(sitemap);
11
10
  } else {
@@ -65,7 +65,16 @@ const widgetValidation = {
65
65
  },
66
66
  url: {
67
67
  isValidURL: (urlValue, urlObj, intlFunc) => {
68
- const urlRegex = /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([_.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gm;
68
+ var urlRegex = new RegExp(
69
+ '^(https?:\\/\\/)?' + // validate protocol
70
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name
71
+ '((\\d{1,3}\\.){3}\\d{1,3}))|' + // validate OR ip (v4) address
72
+ '(localhost)' + // validate OR localhost address
73
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path
74
+ '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string
75
+ '(\\#[-a-z\\d_]*)?$', // validate fragment locator
76
+ 'i',
77
+ );
69
78
  const isValid = urlRegex.test(urlValue);
70
79
  return !isValid ? intlFunc(messages.isValidURL) : null;
71
80
  },
@@ -5,6 +5,7 @@ const schema = {
5
5
  properties: {
6
6
  username: { title: 'Username', type: 'string', description: '' },
7
7
  email: { title: 'Email', type: 'string', widget: 'email', description: '' },
8
+ url: { title: 'url', type: 'string', widget: 'url', description: '' },
8
9
  },
9
10
  fieldsets: [
10
11
  { id: 'default', title: 'FIXME: User Data', fields: ['username'] },
@@ -87,5 +88,45 @@ describe('FormValidation', () => {
87
88
  }),
88
89
  ).toEqual({});
89
90
  });
91
+ it('validates incorrect url', () => {
92
+ formData.url = 'foo';
93
+ expect(
94
+ FormValidation.validateFieldsPerFieldset({
95
+ schema,
96
+ formData,
97
+ formatMessage,
98
+ }),
99
+ ).toEqual({ url: [messages.isValidURL.defaultMessage] });
100
+ });
101
+ it('validates url', () => {
102
+ formData.url = 'https://plone.org/';
103
+ expect(
104
+ FormValidation.validateFieldsPerFieldset({
105
+ schema,
106
+ formData,
107
+ formatMessage,
108
+ }),
109
+ ).toEqual({});
110
+ });
111
+ it('validates url with ip', () => {
112
+ formData.url = 'http://127.0.0.1:8080/Plone';
113
+ expect(
114
+ FormValidation.validateFieldsPerFieldset({
115
+ schema,
116
+ formData,
117
+ formatMessage,
118
+ }),
119
+ ).toEqual({});
120
+ });
121
+ it('validates url with localhost', () => {
122
+ formData.url = 'http://localhost:8080/Plone';
123
+ expect(
124
+ FormValidation.validateFieldsPerFieldset({
125
+ schema,
126
+ formData,
127
+ formatMessage,
128
+ }),
129
+ ).toEqual({});
130
+ });
90
131
  });
91
132
  });
@@ -7,6 +7,7 @@ import { last, memoize } from 'lodash';
7
7
  import { urlRegex, telRegex, mailRegex } from './urlRegex';
8
8
  import prependHttp from 'prepend-http';
9
9
  import config from '@plone/volto/registry';
10
+ import { matchPath } from 'react-router';
10
11
 
11
12
  /**
12
13
  * Get base url.
@@ -213,7 +214,17 @@ export function expandToBackendURL(path) {
213
214
  */
214
215
  export function isInternalURL(url) {
215
216
  const { settings } = config;
216
- return (
217
+
218
+ const isMatch = (config.settings.externalRoutes ?? []).find((route) => {
219
+ if (typeof route === 'object') {
220
+ return matchPath(flattenToAppURL(url), route.match);
221
+ }
222
+ return matchPath(flattenToAppURL(url), route);
223
+ });
224
+
225
+ const isExcluded = isMatch && Object.keys(isMatch)?.length > 0;
226
+
227
+ const internalURL =
217
228
  url &&
218
229
  (url.indexOf(settings.publicURL) !== -1 ||
219
230
  (settings.internalApiPath &&
@@ -221,8 +232,13 @@ export function isInternalURL(url) {
221
232
  url.indexOf(settings.apiPath) !== -1 ||
222
233
  url.charAt(0) === '/' ||
223
234
  url.charAt(0) === '.' ||
224
- url.startsWith('#'))
225
- );
235
+ url.startsWith('#'));
236
+
237
+ if (internalURL && isExcluded) {
238
+ return false;
239
+ }
240
+
241
+ return internalURL;
226
242
  }
227
243
 
228
244
  /**
@@ -191,6 +191,7 @@ describe('Url', () => {
191
191
  expect(isInternalURL(href)).toBe(false);
192
192
  settings.internalApiPath = saved;
193
193
  });
194
+
194
195
  it('tells if an URL is internal if it is an anchor', () => {
195
196
  const href = '#anchor';
196
197
  expect(isInternalURL(href)).toBe(true);
@@ -211,6 +212,17 @@ describe('Url', () => {
211
212
  const href = undefined;
212
213
  expect(isInternalURL(href)).toBe(undefined);
213
214
  });
215
+ it('tells if an URL is external if settings.externalroutes is persent.', () => {
216
+ const url = `https://localhost:3000/fb/my-page/contents`;
217
+ const blacklistedurl = '/blacklisted';
218
+ settings.externalRoutes = [
219
+ { title: 'My Page', match: '/fb' },
220
+ '/blacklisted',
221
+ ];
222
+ settings.publicURL = 'https://localhost:3000';
223
+ expect(isInternalURL(url)).toBe(false);
224
+ expect(isInternalURL(blacklistedurl)).toBe(false);
225
+ });
214
226
  });
215
227
  describe('isUrl', () => {
216
228
  it('isUrl test', () => {
@@ -1,25 +1,59 @@
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
+ };
5
25
 
6
26
  /**
7
27
  * A pagination helper that tracks the query and resets pagination in case the
8
28
  * query changes.
9
29
  */
10
- export const usePagination = (query, defaultPage = 1) => {
11
- const previousQuery = usePrevious(query);
12
- const [currentPage, setCurrentPage] = React.useState(defaultPage);
30
+ export const usePagination = (id = null, defaultPage = 1) => {
31
+ const location = useLocation();
32
+ const history = useHistory();
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);
13
40
 
14
- useDeepCompareEffect(() => {
15
- setCurrentPage(defaultPage);
16
- }, [query, previousQuery, defaultPage]);
41
+ useEffect(() => {
42
+ if (queryRef.current !== qs.parse(location.search)?.query) {
43
+ setCurrentPage(defaultPage);
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]);
17
54
 
18
55
  return {
19
- currentPage:
20
- previousQuery && !isEqual(previousQuery, query)
21
- ? defaultPage
22
- : currentPage,
56
+ currentPage,
23
57
  setCurrentPage,
24
58
  };
25
59
  };
@@ -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
+ });
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ `plone.restapi`
2
+ `plone.volto`
3
+ npm
4
+ Plone
5
+ Razzle
6
+ RichText
7
+ Volto
8
+ Zope
9
+ JavaScript
10
+ NodeJS
@@ -0,0 +1,5 @@
1
+ node
2
+ nodejs
3
+ javascript
4
+ js
5
+ Javascript