@plone/volto 17.0.0-alpha.3 → 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 (44) hide show
  1. package/.changelog.draft +23 -9
  2. package/.yarn/install-state.gz +0 -0
  3. package/CHANGELOG.md +80 -2
  4. package/addon-registry.js +34 -0
  5. package/create-theme-addons-loader.js +79 -0
  6. package/locales/ca/LC_MESSAGES/volto.po +2 -2
  7. package/locales/de/LC_MESSAGES/volto.po +4 -4
  8. package/locales/de.json +1 -1
  9. package/locales/en/LC_MESSAGES/volto.po +2 -2
  10. package/locales/en.json +1 -1
  11. package/locales/es/LC_MESSAGES/volto.po +2 -2
  12. package/locales/eu/LC_MESSAGES/volto.po +2 -2
  13. package/locales/fr/LC_MESSAGES/volto.po +2 -2
  14. package/locales/it/LC_MESSAGES/volto.po +2 -2
  15. package/locales/ja/LC_MESSAGES/volto.po +2 -2
  16. package/locales/nl/LC_MESSAGES/volto.po +2 -2
  17. package/locales/pt/LC_MESSAGES/volto.po +2 -2
  18. package/locales/pt_BR/LC_MESSAGES/volto.po +2 -2
  19. package/locales/ro/LC_MESSAGES/volto.po +2 -2
  20. package/locales/volto.pot +3 -3
  21. package/locales/zh_CN/LC_MESSAGES/volto.po +2 -2
  22. package/package.json +1 -1
  23. package/packages/volto-slate/package.json +1 -1
  24. package/pyvenv.cfg +1 -1
  25. package/razzle.config.js +23 -0
  26. package/src/actions/language/language.js +1 -0
  27. package/src/actions/querystringsearch/querystringsearch.js +20 -14
  28. package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +20 -0
  29. package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +1 -2
  30. package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +8 -4
  31. package/src/components/manage/Widgets/SelectUtils.js +1 -1
  32. package/src/components/manage/Widgets/SelectWidget.jsx +1 -1
  33. package/src/components/theme/PasswordReset/PasswordReset.jsx +1 -1
  34. package/src/components/theme/PasswordReset/RequestPasswordReset.jsx +1 -1
  35. package/src/components/theme/View/DefaultView.jsx +1 -1
  36. package/src/config/Widgets.jsx +1 -0
  37. package/src/config/index.js +1 -0
  38. package/src/express-middleware/sitemap.js +0 -1
  39. package/src/helpers/FormValidation/FormValidation.js +10 -1
  40. package/src/helpers/FormValidation/FormValidation.test.js +41 -0
  41. package/src/helpers/Utils/usePagination.js +48 -14
  42. package/src/helpers/Utils/usePagination.test.js +115 -0
  43. package/styles/Vocab/Plone/accept.txt +2 -0
  44. package/styles/Vocab/Plone/reject.txt +5 -0
@@ -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];
@@ -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
  });
@@ -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
+ });
@@ -6,3 +6,5 @@ Razzle
6
6
  RichText
7
7
  Volto
8
8
  Zope
9
+ JavaScript
10
+ NodeJS
@@ -0,0 +1,5 @@
1
+ node
2
+ nodejs
3
+ javascript
4
+ js
5
+ Javascript