diff --git a/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx new file mode 100644 index 000000000..370ae844d --- /dev/null +++ b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/EndField.jsx @@ -0,0 +1,133 @@ +/** + * EndField component. + * @module components/manage/Widgets/RecurrenceWidget/EndField + * + * + * CUSTOMIZATIONS: + * - added customization to have this changes https://github.com/plone/volto/pull/5555/files + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl } from 'react-intl'; +import { Form, Grid, Input, Radio } from 'semantic-ui-react'; +import DatetimeWidget from '@plone/volto/components/manage/Widgets/DatetimeWidget'; + +const messages = defineMessages({ + recurrenceEnds: { id: 'Recurrence ends', defaultMessage: 'Ends' }, + recurrenceEndsCount: { id: 'Recurrence ends after', defaultMessage: 'after' }, + recurrenceEndsUntil: { id: 'Recurrence ends on', defaultMessage: 'on' }, + occurrences: { id: 'Occurences', defaultMessage: 'occurrence(s)' }, +}); +/** + * EndField component class. + * @function EndField + * @returns {string} Markup of the component. + */ +const EndField = ({ value, count, until, onChange, intl }) => { + return ( + + + + +
+ +
+
+ + + + onChange('recurrenceEnds', value)} + /> + + + {intl.formatMessage(messages.recurrenceEndsCount)} + + + { + onChange( + target.id, + target.value === '' ? undefined : target.value, + ); + }} + /> + + + {intl.formatMessage(messages.occurrences)} + + + + + onChange('recurrenceEnds', value)} + /> + + + + { + onChange(id, value === '' ? undefined : value); + }} + /> + + + +
+
+
+ ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +EndField.propTypes = { + value: PropTypes.string, + count: PropTypes.any, + until: PropTypes.any, + onChange: PropTypes.func, +}; + +/** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ +EndField.defaultProps = { + value: null, + count: null, + until: null, + onChange: null, +}; + +export default injectIntl(EndField); diff --git a/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx new file mode 100644 index 000000000..427701229 --- /dev/null +++ b/src/customizations/volto/components/manage/Widgets/RecurrenceWidget/RecurrenceWidget.jsx @@ -0,0 +1,1025 @@ +/** + * RecurrenceWidget component. + * @module components/manage/Widgets/RecurrenceWidget + * + * * CUSTOMIZATIONS: + * - added customization to have this changes https://github.com/plone/volto/pull/5555/files + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { compose } from 'redux'; +//import { RRule, RRuleSet, rrulestr } from 'rrule'; +import { connect } from 'react-redux'; + +import cx from 'classnames'; +import { isEqual, map, find, concat, remove } from 'lodash'; +import { defineMessages, injectIntl } from 'react-intl'; +import { + Form, + Grid, + Label, + Button, + Segment, + 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'; +import trashSVG from '@plone/volto/icons/delete.svg'; + +import { + Days, + OPTIONS, + FREQUENCES, + WEEKLY_DAYS, + MONDAYFRIDAY_DAYS, + rrulei18n, +} from '@plone/volto/components/manage/Widgets/RecurrenceWidget/Utils'; + +import IntervalField from '@plone/volto/components/manage/Widgets/RecurrenceWidget/IntervalField'; +import ByDayField from '@plone/volto/components/manage/Widgets/RecurrenceWidget/ByDayField'; +import EndField from '@plone/volto/components/manage/Widgets/RecurrenceWidget/EndField'; +import ByMonthField from '@plone/volto/components/manage/Widgets/RecurrenceWidget/ByMonthField'; +import ByYearField from '@plone/volto/components/manage/Widgets/RecurrenceWidget/ByYearField'; +import Occurences from '@plone/volto/components/manage/Widgets/RecurrenceWidget/Occurences'; + +const messages = defineMessages({ + editRecurrence: { + id: 'Edit recurrence', + defaultMessage: 'Edit recurrence', + }, + save: { + id: 'Save recurrence', + defaultMessage: 'Save', + }, + remove: { + id: 'Remove recurrence', + defaultMessage: 'Remove', + }, + repeat: { + id: 'Repeat', + defaultMessage: 'Repeat', + }, + daily: { + id: 'Daily', + defaultMessage: 'Daily', + }, + mondayfriday: { + id: 'Monday and Friday', + defaultMessage: 'Monday and Friday', + }, + + weekdays: { + id: 'Weekday', + defaultMessage: 'Weekday', + }, + weekly: { + id: 'Weekly', + defaultMessage: 'Weekly', + }, + monthly: { + id: 'Monthly', + defaultMessage: 'Monthly', + }, + yearly: { + id: 'Yearly', + defaultMessage: 'Yearly', + }, + + repeatEvery: { + id: 'Repeat every', + defaultMessage: 'Repeat every', + }, + repeatOn: { + id: 'Repeat on', + defaultMessage: 'Repeat on', + }, + + interval_daily: { + id: 'Interval Daily', + defaultMessage: 'days', + }, + interval_weekly: { + id: 'Interval Weekly', + defaultMessage: 'week(s)', + }, + interval_monthly: { + id: 'Interval Monthly', + defaultMessage: 'Month(s)', + }, + interval_yearly: { + id: 'Interval Yearly', + defaultMessage: 'year(s)', + }, + add_date: { + id: 'Add date', + defaultMessage: 'Add date', + }, + select_date_to_add_to_recurrence: { + id: 'Select a date to add to recurrence', + defaultMessage: 'Select a date to add to recurrence', + }, +}); + +const NoRRuleOptions = [ + 'recurrenceEnds', + 'monthly', + 'weekdayOfTheMonthIndex', + 'weekdayOfTheMonth', + 'yearly', + 'monthOfTheYear', + 'byhour', + 'byminute', + 'bysecond', + 'bynmonthday', + 'exdates', + 'rdates', +]; +/** + * RecurrenceWidget component class. + * @function RecurrenceWidget + * @returns {string} Markup of the component. + */ +class RecurrenceWidget extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + id: PropTypes.string.isRequired, + formData: PropTypes.object, + title: PropTypes.string.isRequired, + description: PropTypes.string, + required: PropTypes.bool, + error: PropTypes.arrayOf(PropTypes.string), + value: PropTypes.string, + onChange: PropTypes.func.isRequired, + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + description: null, + required: false, + error: [], + value: null, + }; + + /** + * Constructor + * @method constructor + * @param {Object} props Component properties + * @constructs Actions + */ + constructor(props, intl) { + super(props); + const { RRuleSet, rrulestr } = props.rrule; + + this.moment = this.props.moment.default; + this.moment.locale(toBackendLang(this.props.lang)); + + let rruleSet = this.props.value + ? rrulestr(props.value, { + compatible: true, //If set to True, the parser will operate in RFC-compatible mode. Right now it means that unfold will be turned on, and if a DTSTART is found, it will be considered the first recurrence instance, as documented in the RFC. + forceset: true, + // dtstart: props.formData.start + // ? this.getUTCDate(props.formData.start) + // .startOf('day') + // .toDate() + // : null, + }) + : new RRuleSet(); + + this.state = { + open: false, + rruleSet: rruleSet, + formValues: this.getFormValues(rruleSet), + RRULE_LANGUAGE: rrulei18n( + this.props.intl, + this.moment, + toBackendLang(this.props.lang), + ), + }; + } + + componentDidMount() { + if (this.props.value) { + this.setRecurrenceStartEnd(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.value) { + const changedStart = + prevProps.formData?.start !== this.props.formData?.start; + const changedEnd = prevProps.formData?.end !== this.props.formData?.end; + + 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) { + changeFormValues.until = this.getUTCDate( + this.props.formData?.end, + ).toDate(); + } + this.setState( + (prevState) => { + let rruleSet = prevState.rruleSet; + + rruleSet = this.updateRruleSet( + rruleSet, + { ...prevState.formValues, ...changeFormValues }, + 'dtstart', + start, + ); + + return { + ...prevState, + rruleSet, + }; + }, + () => { + //then, after set state, set recurrence rrule value + this.saveRrule(); + }, + ); + } + } + } + + editRecurrence = () => { + this.setRecurrenceStartEnd(); + }; + + 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(); + + this.setState((prevState) => { + let rruleSet = prevState.rruleSet; + const formValues = this.getFormValues(rruleSet); //to set default values, included end + + rruleSet = this.updateRruleSet(rruleSet, formValues, 'dtstart', _start); + return { + ...prevState, + rruleSet, + formValues, + }; + }); + }; + + getUTCDate = (date) => { + return date.match(/T(.)*(-|\+|Z)/g) + ? this.moment(date).utc() + : this.moment(`${date}Z`).utc(); + }; + + show = (dimmer) => () => { + this.setState({ dimmer, open: true }); + this.editRecurrence(); + }; + close = () => this.setState({ open: false }); + + getFreq = (number, weekdays) => { + let freq = FREQUENCES.DAILY; + Object.entries(OPTIONS.frequences).forEach(([f, o]) => { + if (o.rrule === number) { + freq = f; + } + }); + if (freq === FREQUENCES.WEEKLY && weekdays) { + if (isEqual(weekdays.sort(), WEEKLY_DAYS.map((w) => w.weekday).sort())) { + freq = FREQUENCES.WEEKDAYS; + } + } + return freq; + }; + + getWeekday = (number) => { + var day = null; + const n = number === -1 ? 6 : number; //because sunday for moment has index 0, but for rrule has index 6 + Object.keys(Days).forEach((d) => { + if (Days[d].weekday === n) { + day = Days[d]; + } + }); + return day; + }; + + /** + * Called on init, to populate form values + * */ + getFormValues = (rruleSet) => { + //default + let formValues = { + freq: FREQUENCES.DAILY, + interval: 1, + }; + + formValues = this.changeField( + formValues, + 'recurrenceEnds', + this.props.formData?.end ? 'until' : 'count', + ); + + const rrule = rruleSet.rrules()[0]; + + if (rrule) { + var freq = this.getFreq(rrule.options.freq, rrule.options.byweekday); + + //init with rruleOptions + Object.entries(rrule.options).forEach(([option, value]) => { + switch (option) { + case 'freq': + formValues[option] = freq; + break; + case 'count': + if (value != null) { + formValues['recurrenceEnds'] = option; + formValues[option] = value; + } + break; + case 'until': + if (value != null) { + formValues['recurrenceEnds'] = option; + formValues[option] = value; + } + break; + case 'byweekday': + if (value && value.length > 0) { + if (isEqual(value, WEEKLY_DAYS)) { + formValues['freq'] = FREQUENCES.WEEKDAYS; + } + if (isEqual(value, MONDAYFRIDAY_DAYS)) { + formValues['freq'] = FREQUENCES.MONDAYFRIDAY; + } + } + formValues[option] = value + ? value.map((d) => { + return this.getWeekday(d); + }) + : []; + break; + case 'bymonthday': + if (value && value.length > 0) { + if (freq === FREQUENCES.MONTHLY) { + formValues['monthly'] = option; + } + if (freq === FREQUENCES.YEARLY) { + formValues['yearly'] = option; + } + } else { + if (freq === FREQUENCES.MONTHLY) { + formValues['monthly'] = null; + } + if (freq === FREQUENCES.YEARLY) { + formValues['yearly'] = null; + } + } + formValues[option] = value; + break; + case 'bynweekday': + if (value && value.length > 0) { + //[weekDayNumber,orinal_number] -> translated is for example: [sunday, third] -> the third sunday of the month + + if (freq === FREQUENCES.SMONTHLY) { + formValues['monthly'] = 'byweekday'; + } + if (freq === FREQUENCES.YEARLY) { + formValues['yearly'] = 'byday'; + } + formValues['weekdayOfTheMonth'] = value[0][0]; + formValues['weekdayOfTheMonthIndex'] = value[0][1]; + } + break; + case 'bymonth': + if (freq === FREQUENCES.YEARLY) { + formValues['yearly'] = 'byday'; + } + formValues['monthOfTheYear'] = value ? value[0] : null; + break; + + default: + formValues[option] = value; + } + }); + } + return formValues; + }; + + formValuesToRRuleOptions = (formValues) => { + var values = Object.assign({}, formValues); + + //remove NoRRuleOptions + NoRRuleOptions.forEach((opt) => { + delete values[opt]; + }); + + //transform values for rrule + Object.keys(values).forEach((field) => { + var value = values[field]; + switch (field) { + case 'freq': + if (value) { + value = OPTIONS.frequences[value].rrule; + } + break; + case 'until': + let mDate = null; + if (value) { + mDate = this.moment(new Date(value)); + if (typeof value === 'string') { + mDate = this.moment(new Date(value)); + } else { + //object-->Date() + mDate = this.moment(value); + } + + if (this.props.formData.end) { + //set time from formData.end + const mEnd = this.moment(new Date(this.props.formData.end)); + mDate.set('hour', mEnd.get('hour')); + mDate.set('minute', mEnd.get('minute')); + } + } + value = value ? mDate.toDate() : null; + break; + default: + break; + } + + if (value === 0 || value) { + //set value + values[field] = value; + } else { + //remove empty values + delete values[field]; + } + }); + + return values; + }; + + 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()); + + rruleOptions.dtstart = dstart; + + 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)); + + return set; + }; + + getDefaultUntil = (freq) => { + const moment = this.moment; + var end = this.props.formData?.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; + switch (freq) { + case FREQUENCES.DAILY: + until = end ? end : tomorrow; + break; + case FREQUENCES.WEEKLY: + until = end ? end : nextWeek; + break; + case FREQUENCES.WEEKDAYS: + until = end ? end : nextWeek; + break; + case FREQUENCES.MONDAYFRIDAY: + until = end ? end : nextWeek; + break; + case FREQUENCES.MONTHLY: + until = end ? end : nextMonth; + break; + case FREQUENCES.YEARLY: + until = end ? end : nextYear; + break; + 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'), + ); + + return until; + }; + + changeField = (formValues, field, value) => { + // git p.log('field', field, 'value', value); + //get weekday from state. + const moment = this.moment; + const byweekday = + this.state?.rruleSet?.rrules().length > 0 + ? this.state.rruleSet.rrules()[0].origOptions.byweekday + : null; + const currWeekday = this.getWeekday(moment().day() - 1); + const currMonth = moment().month() + 1; + + const startMonth = this.props.formData?.start + ? moment(this.props.formData.start).month() + 1 + : currMonth; + + const startWeekday = this.props.formData?.start + ? this.getWeekday(moment(this.props.formData.start).day() - 1) + : currWeekday; + formValues[field] = value; + + const defaultMonthDay = this.props.formData?.start + ? moment(this.props.formData.start).date() + : moment().date(); + + switch (field) { + case 'freq': + formValues.interval = 1; + const fconfig = OPTIONS.frequences[value]; + + //clear values + if (!fconfig.interval) { + formValues.interval = null; + } + + formValues = this.changeField(formValues, 'byweekday', null); + formValues = this.changeField(formValues, 'yearly', null); + formValues = this.changeField(formValues, 'bymonthday', null); + formValues = this.changeField(formValues, 'byweekday', null); + formValues = this.changeField(formValues, 'monthOfTheYear', null); + + if (!formValues.until) { + formValues.until = this.getDefaultUntil(value); + } + + //set defaults + switch (value) { + case FREQUENCES.DAILY: + break; + case FREQUENCES.WEEKDAYS: + formValues = this.changeField(formValues, 'byweekday', WEEKLY_DAYS); + break; + case FREQUENCES.MONDAYFRIDAY: + formValues = this.changeField( + formValues, + 'byweekday', + MONDAYFRIDAY_DAYS, + ); + break; + case FREQUENCES.WEEKLY: + formValues = this.changeField(formValues, 'byweekday', [ + startWeekday, + ]); + + break; + case FREQUENCES.MONTHLY: + formValues = this.changeField(formValues, 'monthly', 'bymonthday'); + + break; + case FREQUENCES.YEARLY: + formValues = this.changeField(formValues, 'yearly', 'bymonthday'); + break; + default: + break; + } + + break; + + case 'recurrenceEnds': + if (value === 'count') { + formValues.count = 1; + formValues.until = null; + } + if (value === 'until') { + formValues.until = this.getDefaultUntil(formValues.freq); + formValues.count = null; //default value + } + break; + + case 'byweekday': + formValues.byweekday = value; + + if (FREQUENCES.WEEKLY !== formValues.freq) { + formValues.weekdayOfTheMonth = value ? value[0].weekday : null; + formValues.weekdayOfTheMonthIndex = value ? value[0].n : null; + } else { + delete formValues.weekdayOfTheMonth; + delete formValues.weekdayOfTheMonthIndex; + } + + break; + case 'weekdayOfTheMonth': + var weekday = this.getWeekday(value); // get new day + var n = byweekday ? byweekday[0].n : 1; + //set nth value + formValues.byweekday = weekday ? [weekday.nth(n)] : null; + break; + case 'weekdayOfTheMonthIndex': + var week_day = byweekday ? byweekday[0] : currWeekday; //get day from state. If not set get current day + //set nth value + formValues.byweekday = value ? [week_day.nth(value)] : null; + break; + + case 'monthOfTheYear': + if (value === null || value === undefined) { + delete formValues.bymonth; + } else { + formValues.bymonth = [value]; + } + break; + + case 'monthly': + if (value === 'bymonthday') { + formValues.bymonthday = [defaultMonthDay]; //default value + formValues = this.changeField(formValues, 'byweekday', null); //default value + } + if (value === 'byweekday') { + formValues.bymonthday = null; //default value + formValues = this.changeField(formValues, 'byweekday', [ + currWeekday.nth(1), + ]); //default value + } + if (value === null) { + formValues = this.changeField(formValues, 'bymonthday', null); //default value + formValues = this.changeField(formValues, 'byweekday', null); //default value + } + break; + case 'yearly': + if (value === 'bymonthday') { + //sets bymonth and bymonthday in rruleset + formValues.bymonthday = [defaultMonthDay]; //default value + + formValues = this.changeField( + formValues, + 'monthOfTheYear', + startMonth, + ); //default value: current month + formValues = this.changeField(formValues, 'byweekday', null); //default value + } + if (value === 'byday') { + formValues = this.changeField(formValues, 'bymonthday', null); //default value + formValues = this.changeField(formValues, 'byweekday', [ + startWeekday.nth(1), + ]); //default value + formValues = this.changeField( + formValues, + 'monthOfTheYear', + startMonth, + ); //default value + } + break; + default: + break; + } + return formValues; + }; + + onChangeRule = (field, value) => { + var formValues = Object.assign({}, this.state.formValues); + formValues = this.changeField(formValues, field, value); + + this.setState((prevState) => { + var rruleSet = prevState.rruleSet; + rruleSet = this.updateRruleSet(rruleSet, formValues, field, value); + return { + ...prevState, + rruleSet, + formValues, + }; + }); + }; + + exclude = (date) => { + let list = this.state.rruleSet.exdates().slice(0); + list.push(date); + this.onChangeRule('exdates', list); + }; + + undoExclude = (date) => { + let list = this.state.rruleSet.exdates().slice(0); + remove(list, (e) => { + return e.getTime() === date.getTime(); + }); + this.onChangeRule('exdates', list); + }; + + addDate = (date) => { + const moment = this.moment; + let all = concat(this.state.rruleSet.all(), this.state.rruleSet.exdates()); + + var simpleDate = moment(new Date(date)).startOf('day').toDate().getTime(); + var exists = find(all, (e) => { + var d = moment(e).startOf('day').toDate().getTime(); + return d === simpleDate; + }); + if (!exists) { + let list = this.state.rruleSet.rdates().slice(0); + list.push(new Date(date)); + this.onChangeRule('rdates', list); + } + }; + + saveRrule = () => { + var value = this.state.rruleSet.toString(); + this.props.onChange(this.props.id, value); + }; + + save = () => { + this.saveRrule(); + this.close(); + }; + + remove = () => { + const { RRuleSet } = this.props.rrule; + this.props.onChange(this.props.id, null); + let rruleSet = new RRuleSet(); + this.setState({ + rruleSet: rruleSet, + formValues: this.getFormValues(rruleSet), + }); + }; + + render() { + const { open, dimmer, rruleSet, formValues, RRULE_LANGUAGE } = this.state; + + const { id, title, required, description, error, fieldSet, intl } = + this.props; + + return ( + 0} + className={cx('recurrence-widget', description ? 'help' : '')} + id={`${fieldSet || 'field'}-${id}`} + > + + + +
+ +
+
+ + {rruleSet.rrules()[0] && ( + <> + + + + + + + )} +
+ + {this.props.value && ( + + )} +
+ + + {intl.formatMessage(messages.editRecurrence)}{' '} + + + {rruleSet.rrules().length > 0 && ( + + +
+ {}} + getVocabularyTokenTitle={() => {}} + choices={Object.keys(OPTIONS.frequences).map( + (t) => { + return [t, intl.formatMessage(messages[t])]; + }, + )} + value={formValues.freq} + onChange={this.onChangeRule} + /> + {OPTIONS.frequences[formValues.freq].interval && ( + + )} + + {/***** byday *****/} + {OPTIONS.frequences[formValues.freq].byday && ( + + )} + + {/***** bymonth *****/} + {OPTIONS.frequences[formValues.freq].bymonth && ( + + )} + + {/***** byyear *****/} + {OPTIONS.frequences[formValues.freq].byyear && ( + + )} + + {/*-- ends after N recurrence or date --*/} + + +
+ + + + +
+ {intl.formatMessage(messages.add_date)} +
+ + { + this.addDate(value === '' ? undefined : value); + }} + /> +
+
+ )} +
+ + + +
+ {map(error, (message) => ( + + ))} +
+
+ {description && ( + + +

{description}

+
+
+ )} +
+
+ ); + } +} + +export default compose( + injectLazyLibs(['moment', 'rrule']), + connect((state) => ({ + lang: state.intl.locale, + })), + injectIntl, +)(RecurrenceWidget);