@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.
- package/.changelog.draft +23 -9
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +80 -2
- package/addon-registry.js +34 -0
- package/create-theme-addons-loader.js +79 -0
- package/locales/ca/LC_MESSAGES/volto.po +2 -2
- package/locales/de/LC_MESSAGES/volto.po +4 -4
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +2 -2
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +2 -2
- package/locales/eu/LC_MESSAGES/volto.po +2 -2
- package/locales/fr/LC_MESSAGES/volto.po +2 -2
- package/locales/it/LC_MESSAGES/volto.po +2 -2
- package/locales/ja/LC_MESSAGES/volto.po +2 -2
- package/locales/nl/LC_MESSAGES/volto.po +2 -2
- package/locales/pt/LC_MESSAGES/volto.po +2 -2
- package/locales/pt_BR/LC_MESSAGES/volto.po +2 -2
- package/locales/ro/LC_MESSAGES/volto.po +2 -2
- package/locales/volto.pot +3 -3
- package/locales/zh_CN/LC_MESSAGES/volto.po +2 -2
- package/package.json +1 -1
- package/packages/volto-slate/package.json +1 -1
- package/pyvenv.cfg +1 -1
- package/razzle.config.js +23 -0
- package/src/actions/language/language.js +1 -0
- package/src/actions/querystringsearch/querystringsearch.js +20 -14
- package/src/components/manage/Blocks/Listing/ListingBody.test.jsx +20 -0
- package/src/components/manage/Blocks/Listing/withQuerystringResults.jsx +1 -2
- package/src/components/manage/Blocks/Search/hocs/withSearch.jsx +8 -4
- package/src/components/manage/Widgets/SelectUtils.js +1 -1
- package/src/components/manage/Widgets/SelectWidget.jsx +1 -1
- package/src/components/theme/PasswordReset/PasswordReset.jsx +1 -1
- package/src/components/theme/PasswordReset/RequestPasswordReset.jsx +1 -1
- package/src/components/theme/View/DefaultView.jsx +1 -1
- package/src/config/Widgets.jsx +1 -0
- package/src/config/index.js +1 -0
- package/src/express-middleware/sitemap.js +0 -1
- package/src/helpers/FormValidation/FormValidation.js +10 -1
- package/src/helpers/FormValidation/FormValidation.test.js +41 -0
- package/src/helpers/Utils/usePagination.js +48 -14
- package/src/helpers/Utils/usePagination.test.js +115 -0
- package/styles/Vocab/Plone/accept.txt +2 -0
- 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
|
|
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.
|
|
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
|
-
|
|
189
|
+
search: qs.stringify(newParams),
|
|
186
190
|
});
|
|
187
191
|
}
|
|
188
192
|
},
|
|
189
|
-
[history, oldState, location.
|
|
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}>
|
package/src/config/Widgets.jsx
CHANGED
|
@@ -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,
|
package/src/config/index.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
+
};
|
|
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 = (
|
|
11
|
-
const
|
|
12
|
-
const
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
});
|