@plone/volto 16.20.0 → 16.20.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 CHANGED
@@ -1,26 +1,8 @@
1
- ## 16.20.0 (2023-04-12)
2
-
3
- ### Feature
4
-
5
- - Support RelationList field with named StaticCatalogVocabulary and SelectWidget. @ksuess [#4614](https://github.com/plone/volto/issues/4614)
6
- - Support for declaring a theme in `volto.config.js` or in `package.json`
7
- Add two entry points to allow extension of a theme from other add-ons. @sneridagh [#4625](https://github.com/plone/volto/issues/4625)
8
- - Added querystring search get option. @robgietema [#4658](https://github.com/plone/volto/issues/4658)
1
+ ## 16.20.2 (2023-04-18)
9
2
 
10
3
  ### Bugfix
11
4
 
12
- - Added current page parameter to route in listing and search block pagination - Fix: #3868 @bipoza [#4159](https://github.com/plone/volto/issues/4159)
13
- - Fixed wrong localization on password reset page @iRohitSingh [#4656](https://github.com/plone/volto/issues/4656)
14
- - fix sitemap.xml.gz not is not compressed @dobri1408 [#4663](https://github.com/plone/volto/issues/4663)
15
-
16
- ### Internal
17
-
18
- - Update to p.restapi 8.36.0 and Plone 6.0.3 @sneridagh [#4682](https://github.com/plone/volto/issues/4682)
19
-
20
- ### Documentation
21
-
22
- - 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)
23
- - Improve creating views documentation page. @rboixaderg [#4636](https://github.com/plone/volto/issues/4636)
24
- - Rename "Developer Guidelines" to "Contributing". @stevepiercy [#4666](https://github.com/plone/volto/issues/4666)
25
- - Fix broken link to `ReactJS.org`. @stevepiercy [#4667](https://github.com/plone/volto/issues/4667)
5
+ - Fix robot.txt - the sitemap link should respect x-forwarded headers @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
6
+ - Fix Move to top of folder ordering in folder content view by searching also @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
7
+ - Fix faulty D&D elements in ObjectBrowserList widget @sneridagh [#4703](https://github.com/plone/volto/issues/4703)
26
8
 
package/.eslintrc CHANGED
@@ -44,8 +44,28 @@
44
44
  "rootPathSuffix": "src"
45
45
  }
46
46
  },
47
- "import/core-modules": ["load-volto-addons"]
47
+ "import/core-modules": ["load-volto-addons"],
48
+ "react": {
49
+ "version": "detect"
50
+ }
48
51
  },
52
+ "overrides": [
53
+ {
54
+ "files": ["**/*.ts", "**/*.tsx"],
55
+ "extends": ["plugin:@typescript-eslint/recommended", "react-app", "prettier", "plugin:jsx-a11y/recommended"],
56
+ "plugins": ["@typescript-eslint", "prettier", "react-hooks", "jsx-a11y"],
57
+ "parser": "@typescript-eslint/parser"
58
+ },
59
+ {
60
+ "files": [
61
+ "**/*.stories.js",
62
+ "**/*.stories.jsx"
63
+ ],
64
+ "rules": {
65
+ "import/no-anonymous-default-export": "off"
66
+ }
67
+ }
68
+ ],
49
69
  "globals": {
50
70
  "root": true,
51
71
  "__DEVELOPMENT__": true,
Binary file
package/CHANGELOG.md CHANGED
@@ -8,6 +8,25 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 16.20.2 (2023-04-18)
12
+
13
+ ### Bugfix
14
+
15
+ - Fix robot.txt - the sitemap link should respect x-forwarded headers @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
16
+ - Fix Move to top of folder ordering in folder content view by searching also @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
17
+ - Fix faulty D&D elements in ObjectBrowserList widget @sneridagh [#4703](https://github.com/plone/volto/issues/4703)
18
+
19
+
20
+ ## 16.20.1 (2023-04-14)
21
+
22
+ ### Bugfix
23
+
24
+ - Generate a split sitemap @reebalazs [#4638](https://github.com/plone/volto/issues/4638)
25
+ - Fix Move to top of folder ordering in folder content view @iFlameing [#4690](https://github.com/plone/volto/issues/4690)
26
+ - 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)
27
+ - Fix search block in edit mode re-queries multiple blocks with an empty search text @reebalazs [#4697](https://github.com/plone/volto/issues/4697)
28
+
29
+
11
30
  ## 16.20.0 (2023-04-12)
12
31
 
13
32
  ### Feature
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  }
10
10
  ],
11
11
  "license": "MIT",
12
- "version": "16.20.0",
12
+ "version": "16.20.2",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git@github.com:plone/volto.git"
@@ -49,8 +49,8 @@
49
49
  "stylelint": "./node_modules/.bin/stylelint 'theme/**/*.{css,less}' 'src/**/*.{css,less}'",
50
50
  "stylelint:overrides": "./node_modules/.bin/stylelint 'theme/**/*.overrides' 'src/**/*.overrides'",
51
51
  "stylelint:fix": "yarn stylelint --fix && yarn stylelint:overrides --fix",
52
- "lint": "./node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx,json}'",
53
- "lint:fix": "./node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx,json}'",
52
+ "lint": "./node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx,ts,tsx,json}'",
53
+ "lint:fix": "./node_modules/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx,ts,tsx,json}'",
54
54
  "i18n": "rm -rf build/messages && NODE_ENV=production i18n",
55
55
  "i18n:ci": "yarn i18n && git diff -G'^[^\"POT]' --exit-code",
56
56
  "dry-release": "release-it --dry-run",
@@ -412,6 +412,8 @@
412
412
  "@storybook/addon-essentials": "^6.3.0",
413
413
  "@storybook/addon-links": "^6.3.0",
414
414
  "@storybook/react": "^6.3.0",
415
+ "@typescript-eslint/eslint-plugin": "5.58.0",
416
+ "@typescript-eslint/parser": "5.58.0",
415
417
  "babel-loader": "8.2.2",
416
418
  "full-icu": "1.4.0",
417
419
  "identity-obj-proxy": "3.0.0",
@@ -421,6 +423,7 @@
421
423
  "react-is": "^16.13.1",
422
424
  "release-it": "^15.1.3",
423
425
  "tmp": "0.2.1",
426
+ "typescript": "5.0.4",
424
427
  "use-trace-update": "1.3.2",
425
428
  "why": "0.6.2"
426
429
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "16.20.0",
3
+ "version": "16.20.2",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -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
- const { currentPage, setCurrentPage } = usePagination(data.block, 1);
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
- useDeepCompareEffect(() => {
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
- }, [query, onTriggerSearch]);
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
- // TODO: need to check if SearchableText facet is not already in the query
118
- // Ideally the searchtext functionality should be restructured as being just
119
- // another facet
120
- params.query = params.query.reduce(
121
- // Remove SearchableText from query
122
- (acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
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 hook that will mirror the search block state to a hash location
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.search);
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
- search: qs.stringify(newParams),
189
+ hash: qs.stringify(newParams),
190
190
  });
191
191
  }
192
192
  },
193
- [history, oldState, location.search],
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
- (toSearchText, toSearchFacets, toSortOn, toSortOrder) => {
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,
@@ -796,18 +796,20 @@ class Contents extends Component {
796
796
  */
797
797
  onMoveToTop(event, { value }) {
798
798
  const id = this.state.items[value]['@id'];
799
- value = this.state.currentPage * this.state.pageSize + value;
800
- this.props.orderContent(
801
- getBaseUrl(this.props.pathname),
802
- id.replace(/^.*\//, ''),
803
- -value,
804
- );
805
- this.setState(
806
- {
807
- currentPage: 0,
808
- },
809
- () => this.fetchContents(),
810
- );
799
+ this.props
800
+ .orderContent(
801
+ getBaseUrl(this.props.pathname),
802
+ id.replace(/^.*\//, ''),
803
+ 'top',
804
+ )
805
+ .then(() => {
806
+ this.setState(
807
+ {
808
+ currentPage: 0,
809
+ },
810
+ () => this.fetchContents(),
811
+ );
812
+ });
811
813
  }
812
814
 
813
815
  /**
@@ -818,18 +820,21 @@ class Contents extends Component {
818
820
  * @returns {undefined}
819
821
  */
820
822
  onMoveToBottom(event, { value }) {
821
- this.onOrderItem(
822
- this.state.items[value]['@id'],
823
- value,
824
- this.state.items.length - 1 - value,
825
- false,
826
- );
827
- this.onOrderItem(
828
- this.state.items[value]['@id'],
829
- value,
830
- this.state.items.length - 1 - value,
831
- true,
832
- );
823
+ const id = this.state.items[value]['@id'];
824
+ this.props
825
+ .orderContent(
826
+ getBaseUrl(this.props.pathname),
827
+ id.replace(/^.*\//, ''),
828
+ 'bottom',
829
+ )
830
+ .then(() => {
831
+ this.setState(
832
+ {
833
+ currentPage: 0,
834
+ },
835
+ () => this.fetchContents(),
836
+ );
837
+ });
833
838
  }
834
839
 
835
840
  /**
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
3
  import { Accordion, Button, Segment } from 'semantic-ui-react';
4
4
  import { DragDropList, FormFieldWrapper, Icon } from '@plone/volto/components';
5
- import { applySchemaDefaults } from '@plone/volto/helpers';
5
+ import { applySchemaDefaults, reorderArray } from '@plone/volto/helpers';
6
6
  import ObjectWidget from '@plone/volto/components/manage/Widgets/ObjectWidget';
7
7
 
8
8
  import upSVG from '@plone/volto/icons/up-key.svg';
@@ -164,13 +164,8 @@ const ObjectListWidget = (props) => {
164
164
  if (!destination) {
165
165
  return;
166
166
  }
167
-
168
- const first = value[source.index];
169
- const second = value[destination.index];
170
- value[destination.index] = first;
171
- value[source.index] = second;
172
-
173
- onChange(id, value);
167
+ const newValue = reorderArray(value, source.index, destination.index);
168
+ onChange(id, newValue);
174
169
  return true;
175
170
  }}
176
171
  >
@@ -1,11 +1,34 @@
1
1
  import express from 'express';
2
- import { generateSitemap } from '@plone/volto/helpers';
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
- generateSitemap(req).then((sitemap) => {
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('Content-Disposition', 'attachment; filename="sitemap.xml.gz"');
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
- //const url = `${req.protocol}://${req.get('Host')}`;
19
- const request = superagent.get(
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,27 @@ export const generateRobots = (req) =>
31
28
  if (error) {
32
29
  resolve(text || error);
33
30
  } else {
31
+ // It appears that express does not take the x-forwarded headers into
32
+ // consideration, so we do it ourselves.
33
+ const {
34
+ 'x-forwarded-proto': forwardedProto,
35
+ 'x-forwarded-host': forwardedHost,
36
+ 'x-forwarded-port': forwardedPort,
37
+ } = req.headers;
38
+ const proto = forwardedProto ?? req.protocol;
39
+ const host = forwardedHost ?? req.get('Host');
40
+ const portNum = forwardedPort ?? req.get('Port');
41
+ const port =
42
+ (proto === 'https' && '' + portNum === '443') ||
43
+ (proto === 'http' && '' + portNum === '80')
44
+ ? ''
45
+ : `:${portNum}`;
46
+ // Plone has probably returned the sitemap link with the internal url.
47
+ // If so, let's replace it with the current one.
48
+ const url = `${proto}://${host}${port}`;
49
+ text = text.replace(internalUrl, url);
50
+ // Replace the sitemap with the sitemap index.
51
+ text = text.replace('sitemap.xml.gz', 'sitemap-index.xml');
34
52
  resolve(text);
35
53
  }
36
54
  });
@@ -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=100000000&use_site_search_settings=1`,
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
+ });
@@ -258,11 +258,11 @@ export const removeFromArray = (array, index) => {
258
258
  };
259
259
 
260
260
  /**
261
- * Reorder array
261
+ * Moves an item from origin to target inside an array in an immutable way
262
262
  * @param {Array} array Array with data
263
- * @param {number} origin Index of item to be reordered
264
- * @param {number} target Index of item to be reordered to
265
- * @returns {Array} Array with reordered elements
263
+ * @param {number} origin Index of item to be moved from
264
+ * @param {number} target Index of item to be moved to
265
+ * @returns {Array} Resultant array
266
266
  */
267
267
  export const reorderArray = (array, origin, target) => {
268
268
  const result = Array.from(array);
@@ -1,59 +1,25 @@
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
- };
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 = (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);
10
+ export const usePagination = (query, defaultPage = 1) => {
11
+ const previousQuery = usePrevious(query);
12
+ const [currentPage, setCurrentPage] = React.useState(defaultPage);
40
13
 
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]);
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
  };
@@ -86,6 +86,7 @@ export {
86
86
  hasApiExpander,
87
87
  replaceItemOfArray,
88
88
  cloneDeepSchema,
89
+ reorderArray,
89
90
  } from '@plone/volto/helpers/Utils/Utils';
90
91
  export { messages } from './MessageLabels/MessageLabels';
91
92
  export {
@@ -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
- });