UNPKG

@plone/volto

Version:
1,029 lines (934 loc) 30.9 kB
/** * RecurrenceWidget component. * @module components/manage/Widgets/RecurrenceWidget */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { compose } from 'redux'; import { connect } from 'react-redux'; import cx from 'classnames'; import isEqual from 'lodash/isEqual'; import map from 'lodash/map'; import find from 'lodash/find'; import concat from 'lodash/concat'; import remove from 'lodash/remove'; import { defineMessages, injectIntl } from 'react-intl'; import { Form, Grid, Label, Button, Segment, Modal, Header, } from 'semantic-ui-react'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import DatetimeWidget from '@plone/volto/components/manage/Widgets/DatetimeWidget'; import SelectWidget from '@plone/volto/components/manage/Widgets/SelectWidget'; import { toBackendLang } from '@plone/volto/helpers/Utils/Utils'; 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 './Utils'; import IntervalField from './IntervalField'; import ByDayField from './ByDayField'; import EndField from './EndField'; import ByMonthField from './ByMonthField'; import ByYearField from './ByYearField'; import Occurences from './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 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; // 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 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 ( <Form.Field inline required={required} error={error.length > 0} className={cx('recurrence-widget', description ? 'help' : '')} id={`${fieldSet || 'field'}-${id}`} > <Grid> <Grid.Row stretched verticalAlign="middle"> <Grid.Column width="4"> <div className="wrapper"> <label htmlFor={`field-${id}`}>{title}</label> </div> </Grid.Column> <Grid.Column width="8"> {rruleSet.rrules()[0] && ( <> <Label> {rruleSet.rrules()[0]?.toText( (t) => { return RRULE_LANGUAGE.strings[t]; }, RRULE_LANGUAGE, RRULE_LANGUAGE.dateFormatter, )} </Label> <Segment> <Occurences rruleSet={rruleSet} exclude={this.exclude} undoExclude={this.undoExclude} showTitle={false} editOccurences={false} /> </Segment> </> )} <div> <Button basic disabled={this.props.isDisabled} color="blue" className="edit-recurrence" onClick={this.show('blurring')} type="button" aria-label={intl.formatMessage(messages.editRecurrence)} > <Icon name={editingSVG} size="20px" title={intl.formatMessage(messages.editRecurrence)} /> </Button> {this.props.value && ( <Button basic color="pink" className="remove-recurrence" onClick={() => { this.remove(); }} type="button" aria-label={intl.formatMessage(messages.remove)} > <Icon name={trashSVG} size="20px" title={intl.formatMessage(messages.remove)} /> </Button> )} </div> <Modal dimmer={dimmer} open={open} onClose={this.close} className="recurrence-form" closeIcon > <Modal.Header> {intl.formatMessage(messages.editRecurrence)}{' '} </Modal.Header> <Modal.Content scrolling> {rruleSet.rrules().length > 0 && ( <Modal.Description> <Segment> <Form> <SelectWidget id="freq" title={intl.formatMessage(messages.repeat)} getVocabulary={() => {}} 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 && ( <IntervalField label={intl.formatMessage(messages.repeatEvery)} labelAfter={ formValues.freq && intl.formatMessage( messages['interval_' + formValues.freq], ) } value={formValues.interval} onChange={this.onChangeRule} /> )} {/***** byday *****/} {OPTIONS.frequences[formValues.freq].byday && ( <ByDayField label={intl.formatMessage(messages.repeatOn)} value={formValues.byweekday} onChange={this.onChangeRule} /> )} {/***** bymonth *****/} {OPTIONS.frequences[formValues.freq].bymonth && ( <ByMonthField label={intl.formatMessage(messages.repeatOn)} value={formValues.monthly} bymonthday={formValues.bymonthday} weekdayOfTheMonthIndex={ formValues.weekdayOfTheMonthIndex } weekdayOfTheMonth={formValues.weekdayOfTheMonth} onChange={this.onChangeRule} /> )} {/***** byyear *****/} {OPTIONS.frequences[formValues.freq].byyear && ( <ByYearField label={intl.formatMessage(messages.repeatOn)} value={formValues.yearly} bymonthday={formValues.bymonthday} monthOfTheYear={formValues.monthOfTheYear} weekdayOfTheMonthIndex={ formValues.weekdayOfTheMonthIndex } weekdayOfTheMonth={formValues.weekdayOfTheMonth} onChange={this.onChangeRule} /> )} {/*-- ends after N recurrence or date --*/} <EndField value={formValues.recurrenceEnds} count={formValues.count} until={formValues.until} onChange={this.onChangeRule} /> </Form> </Segment> <Segment> <Occurences rruleSet={rruleSet} exclude={this.exclude} undoExclude={this.undoExclude} /> </Segment> <Segment> <Header as="h2"> {intl.formatMessage(messages.add_date)} </Header> <DatetimeWidget id="addDate" title={intl.formatMessage( messages.select_date_to_add_to_recurrence, )} dateOnly={true} noPastDates={true} onChange={(id, value) => { this.addDate(value === '' ? undefined : value); }} /> </Segment> </Modal.Description> )} </Modal.Content> <Modal.Actions> <Button className="save" basic onClick={() => { this.save(); }} aria-label={intl.formatMessage(messages.save)} > <Icon name={saveSVG} className="circled" size="30px" title={intl.formatMessage(messages.save)} /> </Button> </Modal.Actions> </Modal> {map(error, (message) => ( <Label key={message} basic color="red" pointing> {message} </Label> ))} </Grid.Column> </Grid.Row> {description && ( <Grid.Row stretched> <Grid.Column stretched width="12"> <p className="help">{description}</p> </Grid.Column> </Grid.Row> )} </Grid> </Form.Field> ); } } export default compose( injectLazyLibs(['moment', 'rrule']), connect((state) => ({ lang: state.intl.locale, })), injectIntl, )(RecurrenceWidget);