From 0ad425c162b32372123fd86f6b4655b84f355a91 Mon Sep 17 00:00:00 2001
From: Martina Bustacchini <41484878+deodorhunter@users.noreply.github.com>
Date: Thu, 3 Oct 2024 12:13:50 +0200
Subject: [PATCH] fix: recurrence widget bug when closing and opening multiple
times without saving (#741)
* fix: initial fixes, still broken, cannot save anymore
* fix: completed fixes of RecurrentWidget?
* chore: new test deploy action
* fix: recurrence start date calculations
* chore: RELEASE.md wrong merge strategy by git
* chore: remove test deploy action
---
RELEASE.md | 1 +
.../components/manage/Diff/DiffField.jsx | 5 +-
.../Widgets/RecurrenceWidget/EndField.jsx | 29 +-
.../RecurrenceWidget/RecurrenceWidget.jsx | 349 ++++++++++++------
4 files changed, 254 insertions(+), 130 deletions(-)
diff --git a/RELEASE.md b/RELEASE.md
index 23023f5d0..7da18f518 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -54,6 +54,7 @@
### Fix
- Risolto un problema con il blocco Form che in alcuni casi poteva dare problemi di validazione dei campi di tipo 'testo statico'.
+- Sistemate diverse incogruenze e errori generati dal widget per la creazione delle ricorrenze nel CT Evento.
## Versione 11.23.2 (24/09/2024)
diff --git a/src/customizations/volto/components/manage/Diff/DiffField.jsx b/src/customizations/volto/components/manage/Diff/DiffField.jsx
index 9d06231b4..be70bd62f 100644
--- a/src/customizations/volto/components/manage/Diff/DiffField.jsx
+++ b/src/customizations/volto/components/manage/Diff/DiffField.jsx
@@ -114,7 +114,6 @@ const DiffField = ({
const second = SSRRenderHtml(history, store, two, field);
parts = diff2(first, second);
} else if (schema.type === 'array') {
- // debugger;
const oneArray = (one || []).map((i) => i?.title || i).join(', ');
const twoArray = (two || []).map((j) => j?.title || j).join(', ');
parts = diff2(oneArray, twoArray);
@@ -150,8 +149,8 @@ const DiffField = ({
schema?.type === 'boolean'
? booleanMapping[!!one]
: schema?.widget === 'json'
- ? contentOne
- : one,
+ ? contentOne
+ : one,
schema?.widget ??
(schema?.type === 'object' && field.includes('image')
? field
diff --git a/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx
index d5954d97b..eeea65254 100644
--- a/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx
+++ b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx
@@ -7,7 +7,7 @@
* - added customization to have this changes https://github.com/plone/volto/pull/5555/files
*/
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Form, Grid, Input, Radio } from 'semantic-ui-react';
@@ -25,6 +25,12 @@ const messages = defineMessages({
* @returns {string} Markup of the component.
*/
const EndField = ({ value, count, until, onChange, intl }) => {
+ // Give state to fields, updating logic is convoluted,
+ // update only when/if needed
+ const [occurrenceValue, setOccurrenceValue] = useState(count);
+ const [untilValue, setUntilValue] = useState(until);
+ useEffect(() => setOccurrenceValue(count), [count]);
+ useEffect(() => setUntilValue(until), [until]);
return (
@@ -55,12 +61,12 @@ const EndField = ({ value, count, until, onChange, intl }) => {
{
- onChange(
- target.id,
- target.value === '' ? undefined : target.value,
- );
+ setOccurrenceValue(target.value);
+ if (target.value) {
+ onChange(target.id, parseInt(target.value));
+ }
}}
/>
@@ -86,15 +92,16 @@ const EndField = ({ value, count, until, onChange, intl }) => {
title={intl.formatMessage(messages.recurrenceEndsUntil)}
dateOnly={true}
value={
- until
- ? typeof until === 'string'
- ? until
- : until?.toISOString()
+ untilValue
+ ? typeof untilValue === 'string'
+ ? untilValue
+ : untilValue?.toISOString()
: ''
}
resettable={false}
onChange={(id, value) => {
- onChange(id, value === '' ? undefined : value);
+ setUntilValue(value);
+ if (value) onChange(id, value === '' ? undefined : value);
}}
/>
diff --git a/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx
index 6bcf8c7c4..7f80eee13 100644
--- a/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx
+++ b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx
@@ -1,19 +1,30 @@
/**
* RecurrenceWidget component.
* @module components/manage/Widgets/RecurrenceWidget
- * CUSTOMIZATIONS:
- * - add date field open calendar on top
- * - fix all imports and rrulei18n use
- * - added customization to have this changes https://github.com/plone/volto/pull/5555/files
+ * See https://github.com/RedTurtle/design-comuni-plone-theme/pull/741 for notable changes and reasons
*/
import React, { Component } from 'react';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import moment from 'moment';
-import { RRule, RRuleSet, rrulestr } from 'rrule';
import cx from 'classnames';
-import { isEqual, map, find, concat, remove } from 'lodash';
+import {
+ isEqual,
+ map,
+ find,
+ concat,
+ remove,
+ isNil,
+ isBoolean,
+ isArray,
+ isString,
+ isObject,
+ isEmpty,
+ isNumber,
+ omitBy,
+} from 'lodash';
import { defineMessages, injectIntl } from 'react-intl';
import {
Form,
@@ -24,8 +35,9 @@ import {
Modal,
Header,
} from 'semantic-ui-react';
-
import { SelectWidget, Icon, DatetimeWidget } from '@plone/volto/components';
+import { toBackendLang } from '@plone/volto/helpers';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import saveSVG from '@plone/volto/icons/save.svg';
import editingSVG from '@plone/volto/icons/editing.svg';
@@ -181,8 +193,10 @@ class RecurrenceWidget extends Component {
*/
constructor(props, intl) {
super(props);
+ const { RRuleSet, rrulestr } = props.rrule;
- moment.locale(this.props.intl.locale);
+ this.moment = this.props.moment.default;
+ this.moment.locale(toBackendLang(this.props.lang));
let rruleSet = this.props.value
? rrulestr(props.value, {
@@ -199,11 +213,12 @@ class RecurrenceWidget extends Component {
this.state = {
open: false,
rruleSet: rruleSet,
+ editRruleSet: rruleSet,
formValues: this.getFormValues(rruleSet),
RRULE_LANGUAGE: rrulei18n(
this.props.intl,
- moment,
- this.props.intl.locale,
+ this.moment,
+ toBackendLang(this.props.lang),
),
};
}
@@ -222,7 +237,6 @@ class RecurrenceWidget extends Component {
if (changedStart || changedEnd) {
let start = this.getUTCDate(this.props.formData?.start).toDate();
- // let end = this.getUTCDate(this.props.formData?.end).toDate();
let changeFormValues = {};
if (changedEnd) {
@@ -240,7 +254,6 @@ class RecurrenceWidget extends Component {
'dtstart',
start,
);
-
return {
...prevState,
rruleSet,
@@ -255,6 +268,29 @@ class RecurrenceWidget extends Component {
}
}
+ cleanRRuleObject(rruleObj) {
+ // Recursive function for rrule options object cleanup
+ function cleanObject(obj) {
+ // Use _.omitBy to remove all objectlike falsish and falsish values, preserve dates/string dates/strings/booleans
+ return omitBy(obj, (value) => {
+ if (
+ (isString(value) && !isNaN(Date.parse(value))) ||
+ (value instanceof Date && !isNaN(value.getTime()))
+ )
+ return false;
+ if (isString(value) || isBoolean(value) || isNumber(value))
+ return false;
+ // Everything else goes
+ return (
+ isNil(value) ||
+ (isArray(value) && isEmpty(value)) ||
+ (isObject(value) && isEmpty(value))
+ );
+ });
+ }
+ return cleanObject(rruleObj);
+ }
+
editRecurrence = () => {
this.setRecurrenceStartEnd();
};
@@ -262,25 +298,31 @@ class RecurrenceWidget extends Component {
setRecurrenceStartEnd = () => {
const start = this.props.formData?.start;
- const _start = new Date(start); //The date is already in utc from plone, so this is not necessary: this.getUTCDate(start).startOf('day').toDate();
+ // The `start` date from Plone is in UTC
+ const _start = new Date(start);
this.setState((prevState) => {
- let rruleSet = prevState.rruleSet;
- const formValues = this.getFormValues(rruleSet); //to set default values, included end
+ let editRruleSet = prevState.rruleSet;
+ const formValues = this.getFormValues(editRruleSet); //to set default values, included end
- rruleSet = this.updateRruleSet(rruleSet, formValues, 'dtstart', _start);
+ editRruleSet = this.updateRruleSet(
+ editRruleSet,
+ formValues,
+ 'dtstart',
+ _start,
+ );
return {
...prevState,
- rruleSet,
+ editRruleSet,
formValues,
};
});
};
getUTCDate = (date) => {
- return date.match(/T(.)*(-|\+|Z)/g)
- ? moment(date).utc()
- : moment(`${date}Z`).utc();
+ return date?.match(/T(.)*(-|\+|Z)/g)
+ ? this.moment(date).utc()
+ : this.moment(`${date}Z`).utc();
};
show = (dimmer) => () => {
@@ -320,24 +362,23 @@ class RecurrenceWidget extends Component {
* */
getFormValues = (rruleSet) => {
//default
- let formValues = {
- freq: FREQUENCES.DAILY,
- interval: 1,
- };
-
- formValues = this.changeField(
- formValues,
- 'recurrenceEnds',
- this.props.formData?.end ? 'until' : 'count',
- );
+ let formValues = {};
+ if (!formValues.recurrenceEnds)
+ formValues = this.changeField(
+ formValues,
+ 'recurrenceEnds',
+ this.props.formData?.end && !this.props?.formData?.open_end
+ ? 'until'
+ : 'count',
+ );
const rrule = rruleSet.rrules()[0];
- if (rrule) {
- var freq = this.getFreq(rrule.options.freq, rrule.options.byweekday);
-
+ if (rrule?.options) {
+ const rruleOptions = this.cleanRRuleObject(rrule.options);
+ var freq = this.getFreq(rruleOptions?.freq, rruleOptions?.byweekday);
//init with rruleOptions
- Object.entries(rrule.options).forEach(([option, value]) => {
+ Object.entries(rruleOptions).forEach(([option, value]) => {
switch (option) {
case 'freq':
formValues[option] = freq;
@@ -346,28 +387,31 @@ class RecurrenceWidget extends Component {
if (value != null) {
formValues['recurrenceEnds'] = option;
formValues[option] = value;
+ formValues['until'] = null;
}
break;
case 'until':
- if (value != null) {
- formValues['recurrenceEnds'] = option;
+ // Handle open ended events
+ if (
+ (value !== null && this.props.formData.end) ||
+ this.props.formData.open_end
+ ) {
+ if (value !== null) formValues['recurrenceEnds'] = option;
formValues[option] = value;
}
break;
case 'byweekday':
+ // Correct handling of freq
if (value && value.length > 0) {
if (isEqual(value, WEEKLY_DAYS)) {
formValues['freq'] = FREQUENCES.WEEKDAYS;
- }
- if (isEqual(value, MONDAYFRIDAY_DAYS)) {
+ } else if (isEqual(value, MONDAYFRIDAY_DAYS)) {
formValues['freq'] = FREQUENCES.MONDAYFRIDAY;
- }
- }
- formValues[option] = value
- ? value.map((d) => {
+ } else
+ formValues[option] = value.map((d) => {
return this.getWeekday(d);
- })
- : [];
+ });
+ }
break;
case 'bymonthday':
if (value && value.length > 0) {
@@ -391,7 +435,7 @@ class RecurrenceWidget extends Component {
if (value && value.length > 0) {
//[weekDayNumber,orinal_number] -> translated is for example: [sunday, third] -> the third sunday of the month
- if (freq === FREQUENCES.SMONTHLY) {
+ if (freq === FREQUENCES.MONTHLY) {
formValues['monthly'] = 'byweekday';
}
if (freq === FREQUENCES.YEARLY) {
@@ -406,13 +450,19 @@ class RecurrenceWidget extends Component {
formValues['yearly'] = 'byday';
}
formValues['monthOfTheYear'] = value ? value[0] : null;
+ // Fix bymonth handling
+ formValues[option] = value ? value : null;
break;
-
default:
formValues[option] = value;
}
});
- }
+ } else
+ formValues = {
+ ...formValues,
+ freq: FREQUENCES.DAILY,
+ interval: 1,
+ };
return formValues;
};
@@ -423,7 +473,6 @@ class RecurrenceWidget extends Component {
NoRRuleOptions.forEach((opt) => {
delete values[opt];
});
-
//transform values for rrule
Object.keys(values).forEach((field) => {
var value = values[field];
@@ -436,17 +485,17 @@ class RecurrenceWidget extends Component {
case 'until':
let mDate = null;
if (value) {
- mDate = moment(new Date(value));
+ mDate = this.moment(new Date(value));
if (typeof value === 'string') {
- mDate = moment(new Date(value));
+ mDate = this.moment(new Date(value));
} else {
//object-->Date()
- mDate = moment(value);
+ mDate = this.moment(value);
}
if (this.props.formData.end) {
//set time from formData.end
- const mEnd = moment(new Date(this.props.formData.end));
+ const mEnd = this.moment(new Date(this.props.formData.end));
mDate.set('hour', mEnd.get('hour'));
mDate.set('minute', mEnd.get('minute'));
}
@@ -456,8 +505,7 @@ class RecurrenceWidget extends Component {
default:
break;
}
-
- if (value) {
+ if (value === 0 || value) {
//set value
values[field] = value;
} else {
@@ -470,41 +518,67 @@ class RecurrenceWidget extends Component {
};
updateRruleSet = (rruleSet, formValues, field, value) => {
- var rruleOptions = this.formValuesToRRuleOptions(formValues);
- var dstart =
- field === 'dtstart'
- ? value
- : rruleSet.dtstart()
- ? rruleSet.dtstart()
- : new Date();
- var exdates =
- field === 'exdates' ? value : Object.assign([], rruleSet.exdates());
-
- var rdates =
- field === 'rdates' ? value : Object.assign([], rruleSet.rdates());
+ let rruleOptions = this.formValuesToRRuleOptions(formValues);
+ let dstart = undefined;
+ if (rruleSet.dtstart()) {
+ // Se hai una ricorrenza usala
+ dstart = rruleSet.dtstart();
+ } else {
+ if (this.props.formData.start) {
+ // Verifica chi tra start e end vuoi usare in base al contronto tra le date e al flag fine aperta
+ const mstart = this.moment(new Date(this.props.formData.start));
+ const mend = this.moment(new Date(this.props.formData.end));
+ if (mstart.isSame(mend, 'day')) {
+ dstart = mstart.toDate();
+ } else {
+ if (this.props.formData.open_end) dstart = mstart.toDate();
+ else dstart = mend.toDate();
+ }
+ }
+ }
+ let exdates = Object.assign([], rruleSet.exdates());
+ let rdates = Object.assign([], rruleSet.rdates());
+ if (field === 'dstart') dstart = value;
+ else if (field === 'exdates') exdates = value;
+ else if (field === 'rdates') rdates = value;
+ else if (field === 'freq') {
+ // reset ex and rdates
+ exdates = [];
+ rdates = [];
+ }
rruleOptions.dtstart = dstart;
+ let sanitizedRruleOptions = this.cleanRRuleObject(rruleOptions);
+
+ const { RRule, RRuleSet } = this.props.rrule;
let set = new RRuleSet();
//set.dtstart(dstart);
- set.rrule(new RRule(rruleOptions));
-
- exdates.map((ex) => set.exdate(ex));
- rdates.map((r) => set.rdate(r));
-
+ if (!sanitizedRruleOptions.count && !sanitizedRruleOptions.until) {
+ // Limit to 100, otherwise you can get up tp 500k recurrences, blocking browser
+ // 100 is an arbitrary value
+ sanitizedRruleOptions = { ...sanitizedRruleOptions, count: 100 };
+ }
+ set.rrule(new RRule(sanitizedRruleOptions));
+ for (const ex of exdates) set.exdate(ex);
+ for (const r of rdates) set.rdate(r);
return set;
};
getDefaultUntil = (freq) => {
- var end = this.props.formData?.end
- ? moment(new Date(this.props.formData.end))
- : null;
+ // No clue why this opinionated set of rules
+ // Let's handle open end events
+ const moment = this.moment;
+ let end =
+ this.props.formData?.end && !this.props.formData?.open_end
+ ? moment(new Date(this.props.formData.end))
+ : null;
var tomorrow = moment().add(1, 'days');
var nextWeek = moment().add(7, 'days');
var nextMonth = moment().add(1, 'months');
var nextYear = moment().add(1, 'years');
- var until = end;
+ let until = end;
switch (freq) {
case FREQUENCES.DAILY:
until = end ? end : tomorrow;
@@ -527,38 +601,39 @@ class RecurrenceWidget extends Component {
default:
break;
}
- if (this.props.formData.end) {
- //set default end time
- until.set('hour', end.get('hour'));
- until.set('minute', end.get('minute'));
- }
- until = new Date(
- until.get('year'),
- until.get('month'),
- until.get('date'),
- until.get('hour'),
- until.get('minute'),
- );
+ // if (this.props.formData.end) {
+ // //set default end time
+ // until.set('hour', end.get('hour'));
+ // until.set('minute', end.get('minute'));
+ // }
+ if (until)
+ until = new Date(
+ until.get('year'),
+ until.get('month'),
+ until.get('date'),
+ until.get('hour'),
+ until.get('minute'),
+ );
return until;
};
changeField = (formValues, field, value) => {
- // git p.log('field', field, 'value', value);
//get weekday from state.
- var byweekday =
+ const moment = this.moment;
+ const byweekday =
this.state?.rruleSet?.rrules().length > 0
? this.state.rruleSet.rrules()[0].origOptions.byweekday
: null;
- var currWeekday = this.getWeekday(moment().day() - 1);
- var currMonth = moment().month() + 1;
+ const currWeekday = this.getWeekday(moment().day() - 1);
+ const currMonth = moment().month() + 1;
- var startMonth = this.props.formData?.start
+ const startMonth = this.props.formData?.start
? moment(this.props.formData.start).month() + 1
: currMonth;
- var startWeekday = this.props.formData?.start
+ const startWeekday = this.props.formData?.start
? this.getWeekday(moment(this.props.formData.start).day() - 1)
: currWeekday;
formValues[field] = value;
@@ -583,7 +658,11 @@ class RecurrenceWidget extends Component {
formValues = this.changeField(formValues, 'byweekday', null);
formValues = this.changeField(formValues, 'monthOfTheYear', null);
- if (!formValues.until) {
+ if (
+ !formValues.count &&
+ !formValues.until &&
+ !this.props.formData.open_end
+ ) {
formValues.until = this.getDefaultUntil(value);
}
@@ -656,7 +735,8 @@ class RecurrenceWidget extends Component {
break;
case 'monthOfTheYear':
- if (value === null || value === undefined) {
+ // Fix check, null and undefined are falsish
+ if (!value) {
delete formValues.bymonth;
} else {
formValues.bymonth = [value];
@@ -706,6 +786,10 @@ class RecurrenceWidget extends Component {
default:
break;
}
+ // Somehow it goes missing sometimes, no clue where anymore, force add it
+ if (formValues?.weekdayOfTheMonth && !formValues.monthly) {
+ formValues = { ...formValues, monthly: 'byweekday' };
+ }
return formValues;
};
@@ -714,24 +798,41 @@ class RecurrenceWidget extends Component {
formValues = this.changeField(formValues, field, value);
this.setState((prevState) => {
- var rruleSet = prevState.rruleSet;
- rruleSet = this.updateRruleSet(rruleSet, formValues, field, value);
- return {
- ...prevState,
- rruleSet,
+ var editRruleSet = prevState.editRruleSet;
+ editRruleSet = this.updateRruleSet(
+ editRruleSet,
formValues,
- };
+ field,
+ value,
+ );
+ return editRruleSet
+ ? {
+ ...prevState,
+ editRruleSet,
+ formValues,
+ }
+ : { ...prevState, formValues };
});
};
+ // Handle rdates too, they were left behind :(
exclude = (date) => {
- let list = this.state.rruleSet.exdates().slice(0);
- list.push(date);
- this.onChangeRule('exdates', list);
+ let additionalDates = this.state.editRruleSet.rdates();
+ let exDates = this.state.editRruleSet.exdates();
+
+ if (additionalDates.some((ad) => ad.getTime() === date.getTime())) {
+ remove(additionalDates, (ad) => {
+ return ad.getTime() === date.getTime();
+ });
+ this.onChangeRule('rdates', additionalDates);
+ } else {
+ exDates.push(date);
+ this.onChangeRule('exdates', exDates);
+ }
};
undoExclude = (date) => {
- let list = this.state.rruleSet.exdates().slice(0);
+ let list = this.state.editRruleSet.exdates().slice(0);
remove(list, (e) => {
return e.getTime() === date.getTime();
});
@@ -739,7 +840,11 @@ class RecurrenceWidget extends Component {
};
addDate = (date) => {
- let all = concat(this.state.rruleSet.all(), this.state.rruleSet.exdates());
+ const moment = this.moment;
+ let all = concat(
+ this.state.editRruleSet.all(),
+ this.state.editRruleSet.exdates(),
+ );
var simpleDate = moment(new Date(date)).startOf('day').toDate().getTime();
var exists = find(all, (e) => {
@@ -747,14 +852,18 @@ class RecurrenceWidget extends Component {
return d === simpleDate;
});
if (!exists) {
- let list = this.state.rruleSet.rdates().slice(0);
+ let list = this.state.editRruleSet.rdates().slice(0);
list.push(new Date(date));
this.onChangeRule('rdates', list);
}
};
saveRrule = () => {
- var value = this.state.rruleSet.toString();
+ var value = this.state.editRruleSet.toString();
+ this.setState((prev) => ({
+ ...prev,
+ rruleSet: prev.editRruleSet,
+ }));
this.props.onChange(this.props.id, value);
};
@@ -764,17 +873,19 @@ class RecurrenceWidget extends Component {
};
remove = () => {
+ const { RRuleSet } = this.props.rrule;
this.props.onChange(this.props.id, null);
let rruleSet = new RRuleSet();
this.setState({
+ editRruleSet: rruleSet,
rruleSet: rruleSet,
formValues: this.getFormValues(rruleSet),
});
};
render() {
- const { open, dimmer, rruleSet, formValues, RRULE_LANGUAGE } = this.state;
-
+ const { open, dimmer, rruleSet, editRruleSet, formValues, RRULE_LANGUAGE } =
+ this.state;
const { id, title, required, description, error, fieldSet, intl } =
this.props;
@@ -787,7 +898,7 @@ class RecurrenceWidget extends Component {
id={`${fieldSet || 'field'}-${id}`}
>
-
+
@@ -797,7 +908,7 @@ class RecurrenceWidget extends Component {
{rruleSet.rrules()[0] && (
<>