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] && ( <>