@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.
- package/.changelog.draft +23 -21
- package/.vale.ini +10 -0
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +130 -2
- package/addon-registry.js +34 -0
- package/create-theme-addons-loader.js +79 -0
- package/locales/ca/LC_MESSAGES/volto.po +3 -3
- package/locales/de/LC_MESSAGES/volto.po +5 -5
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +3 -3
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +3 -3
- package/locales/eu/LC_MESSAGES/volto.po +3 -3
- package/locales/fr/LC_MESSAGES/volto.po +3 -3
- package/locales/it/LC_MESSAGES/volto.po +3 -3
- package/locales/ja/LC_MESSAGES/volto.po +3 -3
- package/locales/nl/LC_MESSAGES/volto.po +3 -3
- package/locales/pt/LC_MESSAGES/volto.po +3 -3
- package/locales/pt_BR/LC_MESSAGES/volto.po +3 -3
- package/locales/ro/LC_MESSAGES/volto.po +3 -3
- package/locales/volto.pot +4 -4
- package/locales/zh_CN/LC_MESSAGES/volto.po +3 -3
- 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/BlockChooser/BlockChooser.jsx +6 -2
- 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/Controlpanels/AddonsControlpanel.jsx +3 -3
- 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/Url/Url.js +19 -3
- package/src/helpers/Url/Url.test.js +12 -0
- package/src/helpers/Utils/usePagination.js +48 -14
- package/src/helpers/Utils/usePagination.test.js +115 -0
- package/styles/Vocab/Base/accept.txt +0 -0
- package/styles/Vocab/Base/reject.txt +0 -0
- package/styles/Vocab/Plone/accept.txt +10 -0
- 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,
|
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
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)
|
|
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
|
|
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];
|
|
@@ -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
|
|
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
|
|
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
|
|
|
386
386
|
<a
|
|
387
|
-
href="
|
|
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}>
|
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
|
});
|
package/src/helpers/Url/Url.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
+
});
|
|
File without changes
|
|
File without changes
|