design-comuni-plone-theme 11.1.0 → 11.1.1
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.md +18 -0
- package/RELEASE.md +6 -0
- package/package.json +1 -1
- package/publiccode.yml +2 -2
- package/src/components/ItaliaTheme/Blocks/Listing/Commons/ListingImage.jsx +15 -13
- package/src/components/ItaliaTheme/Blocks/Listing/ContentInEvidenceTemplate.jsx +6 -1
- package/src/components/ItaliaTheme/Blocks/Listing/PhotogalleryTemplate.jsx +12 -1
- package/src/components/ItaliaTheme/Blocks/Listing/SliderTemplate.jsx +1 -1
- package/src/components/ItaliaTheme/Blocks/Listing/SmallBlockLinksTemplate.jsx +5 -1
- package/src/components/ItaliaTheme/Cards/CardPersona.jsx +1 -2
- package/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx +133 -0
- package/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx +81 -31
- package/src/customizations/volto/reducers/navigation/navigation.js +42 -9
- package/src/customizations/volto/middleware/api.js +0 -366
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
+
## [11.1.1](https://github.com/redturtle/design-comuni-plone-theme/compare/v11.1.0...v11.1.1) (2023-12-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* image sizes too small ([b83e3da](https://github.com/redturtle/design-comuni-plone-theme/commit/b83e3da3dffdac76ec8b67f9b9333d840e380762))
|
|
9
|
+
* recurrence widget ([#445](https://github.com/redturtle/design-comuni-plone-theme/issues/445)) ([201a3ac](https://github.com/redturtle/design-comuni-plone-theme/commit/201a3acf5934ff727bcef9053e46c478e339b8f1))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Maintenance
|
|
13
|
+
|
|
14
|
+
* update customizations ([bf8c37f](https://github.com/redturtle/design-comuni-plone-theme/commit/bf8c37fec68811eab25c7777f04320be1bee046a))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* updated publiccode ([5f7a4eb](https://github.com/redturtle/design-comuni-plone-theme/commit/5f7a4eb48a26f96a6653f7cd755ac486782277f8))
|
|
20
|
+
|
|
3
21
|
## [11.1.0](https://github.com/redturtle/design-comuni-plone-theme/compare/v11.0.2...v11.1.0) (2023-12-20)
|
|
4
22
|
|
|
5
23
|
|
package/RELEASE.md
CHANGED
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
- ...
|
|
42
42
|
-->
|
|
43
43
|
|
|
44
|
+
## Versione 11.1.1 (22/12/2023)
|
|
45
|
+
|
|
46
|
+
### Fix
|
|
47
|
+
|
|
48
|
+
- Risolto un problema con la dimensione delle immagini, che risultava in alcuni casi troppo piccola causando immagini sgranate
|
|
49
|
+
|
|
44
50
|
## Versione 11.0.1 (19/12/2023)
|
|
45
51
|
|
|
46
52
|
### Fix
|
package/package.json
CHANGED
package/publiccode.yml
CHANGED
|
@@ -227,9 +227,9 @@ maintenance:
|
|
|
227
227
|
name: io-Comune - Il sito AgID per Comuni ed Enti Pubblici
|
|
228
228
|
platforms:
|
|
229
229
|
- web
|
|
230
|
-
releaseDate: '2023-12-
|
|
230
|
+
releaseDate: '2023-12-22'
|
|
231
231
|
softwareType: standalone/web
|
|
232
|
-
softwareVersion: 11.1.
|
|
232
|
+
softwareVersion: 11.1.1
|
|
233
233
|
url: 'https://github.com/italia/design-comuni-plone-theme'
|
|
234
234
|
usedBy:
|
|
235
235
|
- ASP Comuni Modenesi Area Nord
|
|
@@ -8,25 +8,27 @@ const ListingImage = ({
|
|
|
8
8
|
showDefault = false,
|
|
9
9
|
className = 'listing-image',
|
|
10
10
|
responsive = true,
|
|
11
|
-
sizes = '(max-width:320px) 200px, (max-width:425px) 300px, (max-width:
|
|
11
|
+
sizes = '(max-width:320px) 200px, (max-width:425px) 300px, (max-width:767px) 500px, 410px',
|
|
12
12
|
...imageProps
|
|
13
13
|
}) => {
|
|
14
14
|
const Image = config.getComponent({ name: 'Image' }).component;
|
|
15
15
|
// photogallery needs to check for null image
|
|
16
16
|
// https://stackoverflow.com/questions/33136399/is-there-a-way-to-tell-if-reactelement-renders-null
|
|
17
|
-
const image = Image({
|
|
18
|
-
item,
|
|
19
|
-
'aria-hidden': true,
|
|
20
|
-
alt: '',
|
|
21
|
-
role: 'presentation',
|
|
22
|
-
className,
|
|
23
|
-
loading,
|
|
24
|
-
title: item.title,
|
|
25
|
-
responsive,
|
|
26
|
-
sizes,
|
|
27
|
-
...imageProps
|
|
17
|
+
const image = Image({
|
|
18
|
+
item,
|
|
19
|
+
'aria-hidden': true,
|
|
20
|
+
alt: '',
|
|
21
|
+
role: 'presentation',
|
|
22
|
+
className,
|
|
23
|
+
loading,
|
|
24
|
+
title: item.title,
|
|
25
|
+
responsive,
|
|
26
|
+
sizes,
|
|
27
|
+
...imageProps,
|
|
28
|
+
});
|
|
28
29
|
|
|
29
|
-
if (image === null)
|
|
30
|
+
if (image === null)
|
|
31
|
+
return showDefault ? <img src={DefaultImageSVG} alt="" /> : null;
|
|
30
32
|
|
|
31
33
|
return image;
|
|
32
34
|
};
|
|
@@ -65,7 +65,12 @@ const ContentInEvidenceTemplate = (props) => {
|
|
|
65
65
|
const date = getCalendarDate(item, rrule.rrulestr);
|
|
66
66
|
const eventRecurrenceMore = getEventRecurrenceMore(item, isEditMode);
|
|
67
67
|
const listingText = <ListingText item={item} />;
|
|
68
|
-
const image = ListingImage({
|
|
68
|
+
const image = ListingImage({
|
|
69
|
+
item,
|
|
70
|
+
className: 'item-image',
|
|
71
|
+
loading: 'eager',
|
|
72
|
+
sizes: '(max-width:425px) 400px, (max-width:767px) 520px, 650px',
|
|
73
|
+
});
|
|
69
74
|
const icon = getItemIcon(item);
|
|
70
75
|
const BlockExtraTags = getComponentWithFallback({
|
|
71
76
|
name: 'BlockExtraTags',
|
|
@@ -165,7 +165,18 @@ const PhotogalleryTemplate = ({
|
|
|
165
165
|
<div className="it-carousel-all it-card-bg">
|
|
166
166
|
<Slider {...settings} ref={slider}>
|
|
167
167
|
{items.map((item, i) => {
|
|
168
|
-
const image = ListingImage({
|
|
168
|
+
const image = ListingImage({
|
|
169
|
+
item,
|
|
170
|
+
sizes: `(max-width:600px) 450px, (max-width:1024px) ${
|
|
171
|
+
items.length < 2 ? '1000' : '500'
|
|
172
|
+
}px, ${
|
|
173
|
+
items.length === 1
|
|
174
|
+
? '1300'
|
|
175
|
+
: items.length === 2
|
|
176
|
+
? '650'
|
|
177
|
+
: '450'
|
|
178
|
+
}px`,
|
|
179
|
+
});
|
|
169
180
|
return (
|
|
170
181
|
<div
|
|
171
182
|
className={cx('it-single-slide-wrapper', {
|
|
@@ -353,7 +353,7 @@ const SliderTemplate = ({
|
|
|
353
353
|
const image = ListingImage({
|
|
354
354
|
item,
|
|
355
355
|
loading: index === 0 ? 'eager' : 'lazy',
|
|
356
|
-
sizes: `max-width(
|
|
356
|
+
sizes: `max-width(991px) 620px, ${1300 / nSlidesToShow}px`,
|
|
357
357
|
critical: true,
|
|
358
358
|
});
|
|
359
359
|
return (
|
|
@@ -38,7 +38,11 @@ const SmallBlockLinksTemplate = ({
|
|
|
38
38
|
)}
|
|
39
39
|
<Row className="items">
|
|
40
40
|
{items.map((item, index) => {
|
|
41
|
-
const image = ListingImage({
|
|
41
|
+
const image = ListingImage({
|
|
42
|
+
item,
|
|
43
|
+
sizes: '(max-width:575px) 520px, 200px',
|
|
44
|
+
style: {},
|
|
45
|
+
});
|
|
42
46
|
|
|
43
47
|
return (
|
|
44
48
|
<Col
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EndField component.
|
|
3
|
+
* @module components/manage/Widgets/RecurrenceWidget/EndField
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* * CUSTOMIZATIONS:
|
|
7
|
+
* - added customization to have this changes https://github.com/plone/volto/pull/5555/files
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import PropTypes from 'prop-types';
|
|
12
|
+
import { defineMessages, injectIntl } from 'react-intl';
|
|
13
|
+
import { Form, Grid, Input, Radio } from 'semantic-ui-react';
|
|
14
|
+
import DatetimeWidget from '../DatetimeWidget';
|
|
15
|
+
|
|
16
|
+
const messages = defineMessages({
|
|
17
|
+
recurrenceEnds: { id: 'Recurrence ends', defaultMessage: 'Ends' },
|
|
18
|
+
recurrenceEndsCount: { id: 'Recurrence ends after', defaultMessage: 'after' },
|
|
19
|
+
recurrenceEndsUntil: { id: 'Recurrence ends on', defaultMessage: 'on' },
|
|
20
|
+
occurrences: { id: 'Occurences', defaultMessage: 'occurrence(s)' },
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* EndField component class.
|
|
24
|
+
* @function EndField
|
|
25
|
+
* @returns {string} Markup of the component.
|
|
26
|
+
*/
|
|
27
|
+
const EndField = ({ value, count, until, onChange, intl }) => {
|
|
28
|
+
return (
|
|
29
|
+
<Form.Field inline className="text">
|
|
30
|
+
<Grid>
|
|
31
|
+
<Grid.Row stretched>
|
|
32
|
+
<Grid.Column width="4">
|
|
33
|
+
<div className="wrapper">
|
|
34
|
+
<label htmlFor="recurrenceEnds">
|
|
35
|
+
{intl.formatMessage(messages.recurrenceEnds)}
|
|
36
|
+
</label>
|
|
37
|
+
</div>
|
|
38
|
+
</Grid.Column>
|
|
39
|
+
<Grid.Column width="8">
|
|
40
|
+
<Form.Group inline>
|
|
41
|
+
<Form.Field>
|
|
42
|
+
<Radio
|
|
43
|
+
label=""
|
|
44
|
+
name="recurrenceEnds"
|
|
45
|
+
id="recurrenceEndsCount"
|
|
46
|
+
value="count"
|
|
47
|
+
checked={value === 'count'}
|
|
48
|
+
onChange={(e, { value }) => onChange('recurrenceEnds', value)}
|
|
49
|
+
/>
|
|
50
|
+
</Form.Field>
|
|
51
|
+
<Form.Field disabled={value !== 'count'}>
|
|
52
|
+
{intl.formatMessage(messages.recurrenceEndsCount)}
|
|
53
|
+
</Form.Field>
|
|
54
|
+
<Form.Field disabled={value !== 'count'}>
|
|
55
|
+
<Input
|
|
56
|
+
id="count"
|
|
57
|
+
name="count"
|
|
58
|
+
value={count || ''}
|
|
59
|
+
onChange={({ target }) => {
|
|
60
|
+
onChange(
|
|
61
|
+
target.id,
|
|
62
|
+
target.value === '' ? undefined : target.value,
|
|
63
|
+
);
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
</Form.Field>
|
|
67
|
+
<Form.Field disabled={value !== 'count'}>
|
|
68
|
+
{intl.formatMessage(messages.occurrences)}
|
|
69
|
+
</Form.Field>
|
|
70
|
+
</Form.Group>
|
|
71
|
+
<Form.Group inline>
|
|
72
|
+
<Form.Field>
|
|
73
|
+
<Radio
|
|
74
|
+
id="recurrenceEndsUntil"
|
|
75
|
+
label=""
|
|
76
|
+
name="recurrenceEnds"
|
|
77
|
+
value="until"
|
|
78
|
+
checked={value === 'until'}
|
|
79
|
+
onChange={(e, { value }) => onChange('recurrenceEnds', value)}
|
|
80
|
+
/>
|
|
81
|
+
</Form.Field>
|
|
82
|
+
|
|
83
|
+
<Form.Field disabled={value !== 'until'}>
|
|
84
|
+
<DatetimeWidget
|
|
85
|
+
id="until"
|
|
86
|
+
title={intl.formatMessage(messages.recurrenceEndsUntil)}
|
|
87
|
+
dateOnly={true}
|
|
88
|
+
value={
|
|
89
|
+
until
|
|
90
|
+
? typeof until === 'string'
|
|
91
|
+
? until
|
|
92
|
+
: until?.toISOString()
|
|
93
|
+
: ''
|
|
94
|
+
}
|
|
95
|
+
resettable={false}
|
|
96
|
+
onChange={(id, value) => {
|
|
97
|
+
onChange(id, value === '' ? undefined : value);
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</Form.Field>
|
|
101
|
+
</Form.Group>
|
|
102
|
+
</Grid.Column>
|
|
103
|
+
</Grid.Row>
|
|
104
|
+
</Grid>
|
|
105
|
+
</Form.Field>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Property types.
|
|
111
|
+
* @property {Object} propTypes Property types.
|
|
112
|
+
* @static
|
|
113
|
+
*/
|
|
114
|
+
EndField.propTypes = {
|
|
115
|
+
value: PropTypes.string,
|
|
116
|
+
count: PropTypes.any,
|
|
117
|
+
until: PropTypes.any,
|
|
118
|
+
onChange: PropTypes.func,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Default properties.
|
|
123
|
+
* @property {Object} defaultProps Default properties.
|
|
124
|
+
* @static
|
|
125
|
+
*/
|
|
126
|
+
EndField.defaultProps = {
|
|
127
|
+
value: null,
|
|
128
|
+
count: null,
|
|
129
|
+
until: null,
|
|
130
|
+
onChange: null,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default injectIntl(EndField);
|
package/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* CUSTOMIZATIONS:
|
|
5
5
|
* - add date field open calendar on top
|
|
6
6
|
* - fix all imports and rrulei18n use
|
|
7
|
+
* - added customization to have this changes https://github.com/plone/volto/pull/5555/files
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import React, { Component } from 'react';
|
|
@@ -216,26 +217,41 @@ class RecurrenceWidget extends Component {
|
|
|
216
217
|
|
|
217
218
|
componentDidUpdate(prevProps) {
|
|
218
219
|
if (this.props.value) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
this.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
220
|
+
const changedStart =
|
|
221
|
+
prevProps.formData?.start !== this.props.formData?.start;
|
|
222
|
+
const changedEnd = prevProps.formData?.end !== this.props.formData?.end;
|
|
223
|
+
|
|
224
|
+
if (changedStart || changedEnd) {
|
|
225
|
+
let start = this.getUTCDate(this.props.formData?.start).toDate();
|
|
226
|
+
// let end = this.getUTCDate(this.props.formData?.end).toDate();
|
|
227
|
+
|
|
228
|
+
let changeFormValues = {};
|
|
229
|
+
if (changedEnd) {
|
|
230
|
+
changeFormValues.until = this.getUTCDate(
|
|
231
|
+
this.props.formData?.end,
|
|
232
|
+
).toDate();
|
|
233
|
+
}
|
|
234
|
+
this.setState(
|
|
235
|
+
(prevState) => {
|
|
236
|
+
let rruleSet = prevState.rruleSet;
|
|
237
|
+
|
|
238
|
+
rruleSet = this.updateRruleSet(
|
|
239
|
+
rruleSet,
|
|
240
|
+
{ ...prevState.formValues, ...changeFormValues },
|
|
241
|
+
'dtstart',
|
|
242
|
+
start,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...prevState,
|
|
247
|
+
rruleSet,
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
() => {
|
|
251
|
+
//then, after set state, set recurrence rrule value
|
|
252
|
+
this.saveRrule();
|
|
253
|
+
},
|
|
254
|
+
);
|
|
239
255
|
}
|
|
240
256
|
}
|
|
241
257
|
}
|
|
@@ -247,7 +263,7 @@ class RecurrenceWidget extends Component {
|
|
|
247
263
|
setRecurrenceStartEnd = () => {
|
|
248
264
|
const start = this.props.formData?.start;
|
|
249
265
|
|
|
250
|
-
|
|
266
|
+
const _start = new Date(start); //The date is already in utc from plone, so this is not necessary: this.getUTCDate(start).startOf('day').toDate();
|
|
251
267
|
|
|
252
268
|
this.setState((prevState) => {
|
|
253
269
|
let rruleSet = prevState.rruleSet;
|
|
@@ -336,7 +352,7 @@ class RecurrenceWidget extends Component {
|
|
|
336
352
|
case 'until':
|
|
337
353
|
if (value != null) {
|
|
338
354
|
formValues['recurrenceEnds'] = option;
|
|
339
|
-
formValues[option] =
|
|
355
|
+
formValues[option] = value;
|
|
340
356
|
}
|
|
341
357
|
break;
|
|
342
358
|
case 'byweekday':
|
|
@@ -419,7 +435,24 @@ class RecurrenceWidget extends Component {
|
|
|
419
435
|
}
|
|
420
436
|
break;
|
|
421
437
|
case 'until':
|
|
422
|
-
|
|
438
|
+
let mDate = null;
|
|
439
|
+
if (value) {
|
|
440
|
+
mDate = this.moment(new Date(value));
|
|
441
|
+
if (typeof value === 'string') {
|
|
442
|
+
mDate = this.moment(new Date(value));
|
|
443
|
+
} else {
|
|
444
|
+
//object-->Date()
|
|
445
|
+
mDate = this.moment(value);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (this.props.formData.end) {
|
|
449
|
+
//set time from formData.end
|
|
450
|
+
const mEnd = this.moment(new Date(this.props.formData.end));
|
|
451
|
+
mDate.set('hour', mEnd.get('hour'));
|
|
452
|
+
mDate.set('minute', mEnd.get('minute'));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
value = value ? mDate.toDate() : null;
|
|
423
456
|
break;
|
|
424
457
|
default:
|
|
425
458
|
break;
|
|
@@ -443,8 +476,8 @@ class RecurrenceWidget extends Component {
|
|
|
443
476
|
field === 'dtstart'
|
|
444
477
|
? value
|
|
445
478
|
: rruleSet.dtstart()
|
|
446
|
-
|
|
447
|
-
|
|
479
|
+
? rruleSet.dtstart()
|
|
480
|
+
: new Date();
|
|
448
481
|
var exdates =
|
|
449
482
|
field === 'exdates' ? value : Object.assign([], rruleSet.exdates());
|
|
450
483
|
|
|
@@ -465,12 +498,12 @@ class RecurrenceWidget extends Component {
|
|
|
465
498
|
|
|
466
499
|
getDefaultUntil = (freq) => {
|
|
467
500
|
var end = this.props.formData?.end
|
|
468
|
-
?
|
|
501
|
+
? moment(new Date(this.props.formData.end))
|
|
469
502
|
: null;
|
|
470
|
-
var tomorrow =
|
|
471
|
-
var nextWeek =
|
|
472
|
-
var nextMonth =
|
|
473
|
-
var nextYear =
|
|
503
|
+
var tomorrow = moment().add(1, 'days');
|
|
504
|
+
var nextWeek = moment().add(7, 'days');
|
|
505
|
+
var nextMonth = moment().add(1, 'months');
|
|
506
|
+
var nextYear = moment().add(1, 'years');
|
|
474
507
|
|
|
475
508
|
var until = end;
|
|
476
509
|
switch (freq) {
|
|
@@ -495,6 +528,19 @@ class RecurrenceWidget extends Component {
|
|
|
495
528
|
default:
|
|
496
529
|
break;
|
|
497
530
|
}
|
|
531
|
+
if (this.props.formData.end) {
|
|
532
|
+
//set default end time
|
|
533
|
+
until.set('hour', end.get('hour'));
|
|
534
|
+
until.set('minute', end.get('minute'));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
until = new Date(
|
|
538
|
+
until.get('year'),
|
|
539
|
+
until.get('month'),
|
|
540
|
+
until.get('date'),
|
|
541
|
+
until.get('hour'),
|
|
542
|
+
until.get('minute'),
|
|
543
|
+
);
|
|
498
544
|
|
|
499
545
|
return until;
|
|
500
546
|
};
|
|
@@ -708,9 +754,13 @@ class RecurrenceWidget extends Component {
|
|
|
708
754
|
}
|
|
709
755
|
};
|
|
710
756
|
|
|
711
|
-
|
|
757
|
+
saveRrule = () => {
|
|
712
758
|
var value = this.state.rruleSet.toString();
|
|
713
759
|
this.props.onChange(this.props.id, value);
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
save = () => {
|
|
763
|
+
this.saveRrule();
|
|
714
764
|
this.close();
|
|
715
765
|
};
|
|
716
766
|
|
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { map } from 'lodash';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
flattenToAppURL,
|
|
14
|
+
getBaseUrl,
|
|
15
|
+
hasApiExpander,
|
|
16
|
+
} from '@plone/volto/helpers';
|
|
13
17
|
|
|
14
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
GET_CONTENT,
|
|
20
|
+
GET_NAVIGATION,
|
|
21
|
+
} from '@plone/volto/constants/ActionTypes';
|
|
15
22
|
|
|
16
23
|
const initialState = {
|
|
17
24
|
error: null,
|
|
@@ -30,6 +37,7 @@ const initialState = {
|
|
|
30
37
|
function getRecursiveItems(items) {
|
|
31
38
|
return map(items, (item) => ({
|
|
32
39
|
title: item.title,
|
|
40
|
+
description: item.description,
|
|
33
41
|
url: item.remoteUrl ?? flattenToAppURL(item['@id']),
|
|
34
42
|
...(item.items && { items: getRecursiveItems(item.items) }),
|
|
35
43
|
}));
|
|
@@ -43,6 +51,7 @@ function getRecursiveItems(items) {
|
|
|
43
51
|
* @returns {Object} New state.
|
|
44
52
|
*/
|
|
45
53
|
export default function navigation(state = initialState, action = {}) {
|
|
54
|
+
let hasExpander;
|
|
46
55
|
switch (action.type) {
|
|
47
56
|
case `${GET_NAVIGATION}_PENDING`:
|
|
48
57
|
return {
|
|
@@ -51,14 +60,38 @@ export default function navigation(state = initialState, action = {}) {
|
|
|
51
60
|
loaded: false,
|
|
52
61
|
loading: true,
|
|
53
62
|
};
|
|
63
|
+
case `${GET_CONTENT}_SUCCESS`:
|
|
64
|
+
hasExpander = hasApiExpander(
|
|
65
|
+
'navigation',
|
|
66
|
+
getBaseUrl(flattenToAppURL(action.result['@id'])),
|
|
67
|
+
);
|
|
68
|
+
if (hasExpander && !action.subrequest) {
|
|
69
|
+
return {
|
|
70
|
+
...state,
|
|
71
|
+
error: null,
|
|
72
|
+
items: getRecursiveItems(
|
|
73
|
+
action.result['@components'].navigation.items,
|
|
74
|
+
),
|
|
75
|
+
loaded: true,
|
|
76
|
+
loading: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return state;
|
|
54
80
|
case `${GET_NAVIGATION}_SUCCESS`:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
// Even if the expander is set or not, if the GET_NAVIGATION is
|
|
82
|
+
// called, we want it to store the data if the actions data is
|
|
83
|
+
// not set in the expander data (['@components']) but in the "normal"
|
|
84
|
+
// action result (we look for the object property returned by the endpoint)
|
|
85
|
+
if (!action.result?.['@components'] && action.result?.items) {
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
error: null,
|
|
89
|
+
items: getRecursiveItems(action.result.items),
|
|
90
|
+
loaded: true,
|
|
91
|
+
loading: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return state;
|
|
62
95
|
case `${GET_NAVIGATION}_FAIL`:
|
|
63
96
|
return {
|
|
64
97
|
...state,
|
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* backport parziale/temporaneo di https://github.com/plone/volto/pull/5069
|
|
3
|
-
* rimuovere dopo l'aggiornamento a Volto >= 17.0.0-alpha.24
|
|
4
|
-
*
|
|
5
|
-
* Api middleware.
|
|
6
|
-
* @module middleware/api
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import Cookies from 'universal-cookie';
|
|
10
|
-
import jwtDecode from 'jwt-decode';
|
|
11
|
-
import { compact, flatten, union } from 'lodash';
|
|
12
|
-
import { matchPath } from 'react-router';
|
|
13
|
-
import qs from 'query-string';
|
|
14
|
-
|
|
15
|
-
import config from '@plone/volto/registry';
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
GET_CONTENT,
|
|
19
|
-
LOGIN,
|
|
20
|
-
RESET_APIERROR,
|
|
21
|
-
SET_APIERROR,
|
|
22
|
-
} from '@plone/volto/constants/ActionTypes';
|
|
23
|
-
import { changeLanguage } from '@plone/volto/actions';
|
|
24
|
-
import {
|
|
25
|
-
toGettextLang,
|
|
26
|
-
toReactIntlLang,
|
|
27
|
-
getCookieOptions,
|
|
28
|
-
} from '@plone/volto/helpers';
|
|
29
|
-
let socket = null;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
* Add configured expanders to an api call for an action
|
|
34
|
-
* Requirements:
|
|
35
|
-
*
|
|
36
|
-
* - It should add the expanders set in the config settings
|
|
37
|
-
* - It should preserve any query if present
|
|
38
|
-
* - It should preserve (and add) any expand parameter (if present) e.g. translations
|
|
39
|
-
* - It should take use the correct codification for arrays in querystring (repeated parameter for each member of the array)
|
|
40
|
-
*
|
|
41
|
-
* @function addExpandersToPath
|
|
42
|
-
* @param {string} path The url/path including the querystring
|
|
43
|
-
* @param {*} type The action type
|
|
44
|
-
* @returns {string} The url/path with the configured expanders added to the query string
|
|
45
|
-
*/
|
|
46
|
-
export function addExpandersToPath(path, type, isAnonymous) {
|
|
47
|
-
const { settings } = config;
|
|
48
|
-
const { apiExpanders = [] } = settings;
|
|
49
|
-
|
|
50
|
-
const {
|
|
51
|
-
url,
|
|
52
|
-
query: { expand, ...query },
|
|
53
|
-
} = qs.parseUrl(path, { decode: false });
|
|
54
|
-
|
|
55
|
-
const expandersFromConfig = apiExpanders
|
|
56
|
-
.filter((expand) => matchPath(url, expand.match) && expand[type])
|
|
57
|
-
.map((expand) => expand[type]);
|
|
58
|
-
|
|
59
|
-
const expandMerge = compact(
|
|
60
|
-
union([expand, ...flatten(expandersFromConfig)]),
|
|
61
|
-
).filter((item) => !(item === 'types' && isAnonymous)); // Remove types expander if isAnonymous
|
|
62
|
-
|
|
63
|
-
const stringifiedExpand = qs.stringify(
|
|
64
|
-
{ expand: expandMerge },
|
|
65
|
-
{
|
|
66
|
-
arrayFormat: 'comma',
|
|
67
|
-
encode: false,
|
|
68
|
-
},
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const querystringFromConfig = apiExpanders
|
|
72
|
-
.filter((expand) => matchPath(url, expand.match) && expand[type])
|
|
73
|
-
.reduce((acc, expand) => {
|
|
74
|
-
let querystring = expand?.['querystring'];
|
|
75
|
-
// The querystring accepts being a function to be able to take other
|
|
76
|
-
// config parameters
|
|
77
|
-
if (typeof querystring === 'function') {
|
|
78
|
-
querystring = querystring(config);
|
|
79
|
-
}
|
|
80
|
-
return { ...acc, ...querystring };
|
|
81
|
-
}, {});
|
|
82
|
-
|
|
83
|
-
const queryMerge = { ...query, ...querystringFromConfig };
|
|
84
|
-
|
|
85
|
-
const stringifiedQuery = qs.stringify(queryMerge, {
|
|
86
|
-
encode: false,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (stringifiedQuery && stringifiedExpand) {
|
|
90
|
-
return `${url}?${stringifiedExpand}&${stringifiedQuery}`;
|
|
91
|
-
} else if (!stringifiedQuery && stringifiedExpand) {
|
|
92
|
-
return `${url}?${stringifiedExpand}`;
|
|
93
|
-
} else if (stringifiedQuery && !stringifiedExpand) {
|
|
94
|
-
return `${url}?${stringifiedQuery}`;
|
|
95
|
-
} else {
|
|
96
|
-
return url;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Send a message on a websocket.
|
|
102
|
-
* @function sendOnSocket
|
|
103
|
-
* @param {Object} request Request object.
|
|
104
|
-
* @returns {Promise} message is send
|
|
105
|
-
*/
|
|
106
|
-
function sendOnSocket(request) {
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
switch (socket.readyState) {
|
|
109
|
-
case socket.CONNECTING:
|
|
110
|
-
socket.addEventListener('open', () => resolve(socket));
|
|
111
|
-
socket.addEventListener('error', reject);
|
|
112
|
-
break;
|
|
113
|
-
case socket.OPEN:
|
|
114
|
-
resolve(socket);
|
|
115
|
-
break;
|
|
116
|
-
default:
|
|
117
|
-
reject();
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}).then(() => {
|
|
121
|
-
socket.send(JSON.stringify(request));
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Api middleware.
|
|
127
|
-
* @function
|
|
128
|
-
* @param {Object} api Api object.
|
|
129
|
-
* @returns {Promise} Action promise.
|
|
130
|
-
*/
|
|
131
|
-
const apiMiddlewareFactory =
|
|
132
|
-
(api) =>
|
|
133
|
-
({ dispatch, getState }) =>
|
|
134
|
-
(next) =>
|
|
135
|
-
(action) => {
|
|
136
|
-
const { settings } = config;
|
|
137
|
-
|
|
138
|
-
const token = getState().userSession.token;
|
|
139
|
-
let isAnonymous = true;
|
|
140
|
-
if (token) {
|
|
141
|
-
const tokenExpiration = jwtDecode(token).exp;
|
|
142
|
-
const currentTime = new Date().getTime() / 1000;
|
|
143
|
-
isAnonymous = !token || currentTime > tokenExpiration;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (typeof action === 'function') {
|
|
147
|
-
return action(dispatch, getState);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const { request, type, mode = 'parallel', ...rest } = action;
|
|
151
|
-
const { subrequest } = action; // We want subrequest remains in `...rest` above
|
|
152
|
-
|
|
153
|
-
let actionPromise;
|
|
154
|
-
|
|
155
|
-
if (!request) {
|
|
156
|
-
return next(action);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
next({ ...rest, type: `${type}_PENDING` });
|
|
160
|
-
|
|
161
|
-
if (socket) {
|
|
162
|
-
actionPromise = Array.isArray(request)
|
|
163
|
-
? Promise.all(
|
|
164
|
-
request.map((item) =>
|
|
165
|
-
sendOnSocket({
|
|
166
|
-
...item,
|
|
167
|
-
path: addExpandersToPath(item.path, type, isAnonymous),
|
|
168
|
-
id: type,
|
|
169
|
-
}),
|
|
170
|
-
),
|
|
171
|
-
)
|
|
172
|
-
: sendOnSocket({
|
|
173
|
-
...request,
|
|
174
|
-
path: addExpandersToPath(request.path, type, isAnonymous),
|
|
175
|
-
id: type,
|
|
176
|
-
});
|
|
177
|
-
} else {
|
|
178
|
-
actionPromise = Array.isArray(request)
|
|
179
|
-
? mode === 'serial'
|
|
180
|
-
? request.reduce((prevPromise, item) => {
|
|
181
|
-
return prevPromise.then((acc) => {
|
|
182
|
-
return api[item.op](
|
|
183
|
-
addExpandersToPath(item.path, type, isAnonymous),
|
|
184
|
-
{
|
|
185
|
-
data: item.data,
|
|
186
|
-
type: item.type,
|
|
187
|
-
headers: item.headers,
|
|
188
|
-
params: request.params,
|
|
189
|
-
checkUrl: settings.actions_raising_api_errors.includes(
|
|
190
|
-
action.type,
|
|
191
|
-
),
|
|
192
|
-
},
|
|
193
|
-
).then((reqres) => {
|
|
194
|
-
return [...acc, reqres];
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
}, Promise.resolve([]))
|
|
198
|
-
: Promise.all(
|
|
199
|
-
request.map((item) =>
|
|
200
|
-
api[item.op](addExpandersToPath(item.path, type, isAnonymous), {
|
|
201
|
-
data: item.data,
|
|
202
|
-
type: item.type,
|
|
203
|
-
headers: item.headers,
|
|
204
|
-
params: request.params,
|
|
205
|
-
checkUrl: settings.actions_raising_api_errors.includes(
|
|
206
|
-
action.type,
|
|
207
|
-
),
|
|
208
|
-
}),
|
|
209
|
-
),
|
|
210
|
-
)
|
|
211
|
-
: api[request.op](addExpandersToPath(request.path, type, isAnonymous), {
|
|
212
|
-
data: request.data,
|
|
213
|
-
type: request.type,
|
|
214
|
-
headers: request.headers,
|
|
215
|
-
params: request.params,
|
|
216
|
-
checkUrl: settings.actions_raising_api_errors.includes(action.type),
|
|
217
|
-
});
|
|
218
|
-
actionPromise.then(
|
|
219
|
-
(result) => {
|
|
220
|
-
const { settings } = config;
|
|
221
|
-
if (getState().apierror.connectionRefused) {
|
|
222
|
-
next({
|
|
223
|
-
...rest,
|
|
224
|
-
type: RESET_APIERROR,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
if (type === GET_CONTENT) {
|
|
228
|
-
const lang = result?.language?.token;
|
|
229
|
-
if (
|
|
230
|
-
lang &&
|
|
231
|
-
getState().intl.locale !== toReactIntlLang(lang) &&
|
|
232
|
-
!subrequest &&
|
|
233
|
-
config.settings.supportedLanguages.includes(lang)
|
|
234
|
-
) {
|
|
235
|
-
const langFileName = toGettextLang(lang);
|
|
236
|
-
import('~/../locales/' + langFileName + '.json').then(
|
|
237
|
-
(locale) => {
|
|
238
|
-
dispatch(changeLanguage(lang, locale.default));
|
|
239
|
-
},
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (type === LOGIN && settings.websockets) {
|
|
244
|
-
const cookies = new Cookies();
|
|
245
|
-
cookies.set(
|
|
246
|
-
'auth_token',
|
|
247
|
-
result.token,
|
|
248
|
-
getCookieOptions({
|
|
249
|
-
expires: new Date(jwtDecode(result.token).exp * 1000),
|
|
250
|
-
}),
|
|
251
|
-
);
|
|
252
|
-
api.get('/@wstoken').then((res) => {
|
|
253
|
-
socket = new WebSocket(
|
|
254
|
-
`${settings.apiPath.replace('http', 'ws')}/@ws?ws_token=${
|
|
255
|
-
res.token
|
|
256
|
-
}`,
|
|
257
|
-
);
|
|
258
|
-
socket.onmessage = (message) => {
|
|
259
|
-
const packet = JSON.parse(message.data);
|
|
260
|
-
if (packet.error) {
|
|
261
|
-
dispatch({
|
|
262
|
-
type: `${packet.id}_FAIL`,
|
|
263
|
-
error: packet.error,
|
|
264
|
-
});
|
|
265
|
-
} else {
|
|
266
|
-
dispatch({
|
|
267
|
-
type: `${packet.id}_SUCCESS`,
|
|
268
|
-
result: JSON.parse(packet.data),
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
try {
|
|
275
|
-
return next({ ...rest, result, type: `${type}_SUCCESS` });
|
|
276
|
-
} catch (error) {
|
|
277
|
-
// There was an exception while processing reducers or downstream middleware.
|
|
278
|
-
next({
|
|
279
|
-
...rest,
|
|
280
|
-
error: { status: 500, error },
|
|
281
|
-
type: `${type}_FAIL`,
|
|
282
|
-
});
|
|
283
|
-
// Rethrow the original exception on the client side only,
|
|
284
|
-
// so it doesn't fall through to express on the server.
|
|
285
|
-
if (__CLIENT__) throw error;
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
(error) => {
|
|
289
|
-
// Only SSR can set ECONNREFUSED
|
|
290
|
-
if (error.code === 'ECONNREFUSED') {
|
|
291
|
-
next({
|
|
292
|
-
...rest,
|
|
293
|
-
error,
|
|
294
|
-
statusCode: error.code,
|
|
295
|
-
connectionRefused: true,
|
|
296
|
-
type: SET_APIERROR,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Response error is marked crossDomain if CORS error happen
|
|
301
|
-
else if (error.crossDomain) {
|
|
302
|
-
next({
|
|
303
|
-
...rest,
|
|
304
|
-
error,
|
|
305
|
-
statusCode: 'CORSERROR',
|
|
306
|
-
connectionRefused: false,
|
|
307
|
-
type: SET_APIERROR,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Check for actions who can raise api errors
|
|
312
|
-
if (settings.actions_raising_api_errors.includes(action.type)) {
|
|
313
|
-
// Gateway timeout
|
|
314
|
-
if (error?.response?.statusCode === 504) {
|
|
315
|
-
next({
|
|
316
|
-
...rest,
|
|
317
|
-
error,
|
|
318
|
-
statusCode: error.code,
|
|
319
|
-
connectionRefused: true,
|
|
320
|
-
type: SET_APIERROR,
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Redirect
|
|
325
|
-
else if (error?.code === 301) {
|
|
326
|
-
next({
|
|
327
|
-
...rest,
|
|
328
|
-
error,
|
|
329
|
-
statusCode: error.code,
|
|
330
|
-
connectionRefused: false,
|
|
331
|
-
type: SET_APIERROR,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Redirect
|
|
336
|
-
else if (error?.code === 408) {
|
|
337
|
-
next({
|
|
338
|
-
...rest,
|
|
339
|
-
error,
|
|
340
|
-
statusCode: error.code,
|
|
341
|
-
connectionRefused: false,
|
|
342
|
-
type: SET_APIERROR,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Unauthorized
|
|
347
|
-
else if (error?.response?.statusCode === 401) {
|
|
348
|
-
next({
|
|
349
|
-
...rest,
|
|
350
|
-
error,
|
|
351
|
-
statusCode: error.response,
|
|
352
|
-
message: error.response.body.message,
|
|
353
|
-
connectionRefused: false,
|
|
354
|
-
type: SET_APIERROR,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return next({ ...rest, error, type: `${type}_FAIL` });
|
|
359
|
-
},
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return actionPromise;
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
export default apiMiddlewareFactory;
|