Merge: implement rrule support when editing events

This is a merge of #73 in addition to some fixes and cleanups.
There are still things to be done, especially in the design department,
but things are at least working and usable.
master
Tom Hacohen 5 years ago
commit 09ff87ba33

@ -33,6 +33,8 @@ import * as EteSync from 'etesync';
import { getCurrentTimezone } from '../helpers';
import { EventType, timezoneLoadFromName } from '../pim-types';
import RRule, { RRuleOptions } from '../widgets/RRule';
interface PropsType {
collections: EteSync.CollectionInfo[];
@ -52,6 +54,7 @@ class EventEdit extends React.PureComponent<PropsType> {
start?: Date;
end?: Date;
timezone: string | null;
rrule?: RRuleOptions;
location: string;
description: string;
journalUid: string;
@ -104,6 +107,7 @@ class EventEdit extends React.PureComponent<PropsType> {
this.state.location = event.location ? event.location : '';
this.state.description = event.description ? event.description : '';
this.state.timezone = event.timezone;
this.state.rrule = this.props.item?.component.getFirstPropertyValue<ICAL.Recur>('rrule')?.toJSON();
} else {
this.state.uid = uuid.v4();
}
@ -121,6 +125,8 @@ class EventEdit extends React.PureComponent<PropsType> {
this.handleInputChange = this.handleInputChange.bind(this);
this.toggleAllDay = this.toggleAllDay.bind(this);
this.onDeleteRequest = this.onDeleteRequest.bind(this);
this.toggleRecurring = this.toggleRecurring.bind(this);
this.handleRRuleChange = this.handleRRuleChange.bind(this);
}
public UNSAFE_componentWillReceiveProps(nextProps: any) {
@ -154,7 +160,15 @@ class EventEdit extends React.PureComponent<PropsType> {
public toggleAllDay() {
this.setState({ allDay: !this.state.allDay });
}
public toggleRecurring() {
const value = this.state.rrule ? undefined : { freq: 'WEEKLY', interval: 1 };
this.setState({ rrule: value });
}
public handleRRuleChange(rrule: RRuleOptions): void {
this.setState({ rrule: rrule });
}
public onSubmit(e: React.FormEvent<any>) {
e.preventDefault();
@ -191,6 +205,7 @@ class EventEdit extends React.PureComponent<PropsType> {
:
new EventType()
;
event.uid = this.state.uid;
event.summary = this.state.title;
event.startDate = startDate;
@ -204,6 +219,18 @@ class EventEdit extends React.PureComponent<PropsType> {
event.endDate = event.endDate?.convertToZone(timezone);
}
}
if (this.state.rrule) {
const rruleData: ICAL.RecurData = {};
for (const key of Object.keys(this.state.rrule)) {
const value = this.state.rrule[key];
if ((value === undefined) || (value?.length === 0)) {
continue;
}
rruleData[key] = value;
}
event.component.updatePropertyWithValue('rrule', new ICAL.Recur(rruleData));
}
event.component.updatePropertyWithValue('last-modified', ICAL.Time.now());
@ -236,7 +263,7 @@ class EventEdit extends React.PureComponent<PropsType> {
const differentTimezone = this.state.timezone && (this.state.timezone !== getCurrentTimezone()) && timezoneLoadFromName(this.state.timezone);
return (
<React.Fragment>
<>
<h2>
{this.props.item ? 'Edit Event' : 'New Event'}
</h2>
@ -335,7 +362,25 @@ class EventEdit extends React.PureComponent<PropsType> {
value={this.state.description}
onChange={this.handleInputChange}
/>
<FormGroup>
<FormControlLabel
control={
<Switch
name="recurring"
checked={!!this.state.rrule}
onChange={this.toggleRecurring}
color="primary"
/>
}
label="Recurring"
/>
</FormGroup>
{this.state.rrule &&
<RRule
onChange={this.handleRRuleChange}
rrule={this.state.rrule ? this.state.rrule : { freq: 'DAILY', interval: 1 }}
/>
}
<div style={styles.submit}>
<Button
variant="contained"
@ -380,9 +425,9 @@ class EventEdit extends React.PureComponent<PropsType> {
onOk={() => this.props.onDelete(this.props.item!, this.props.initialCollection!)}
onCancel={() => this.setState({ showDeleteDialog: false })}
>
Are you sure you would like to delete this event?
Are you sure you would like to delete this event?
</ConfirmationDialog>
</React.Fragment>
</>
);
}
}

@ -173,7 +173,8 @@ declare module 'ical.js' {
public bysetpos?: number[];
}
export class Recur extends RecurData {
export class Recur {
constructor(data?: RecurData);
public toJSON(): RecurData;
}
}

@ -1,40 +1,16 @@
import * as React from 'react';
import { TextField, Select, MenuItem, FormGroup, FormControlLabel, Checkbox, InputLabel, FormControl } from '@material-ui/core';
import { TextField, Select, MenuItem, FormControlLabel, InputLabel, FormControl } from '@material-ui/core';
import DateTimePicker from '../widgets/DateTimePicker';
import { isNumber } from 'util';
import * as ICAL from 'ical.js';
interface PropsType {
onChange: (rrule: RRuleOptions) => void;
rrule: RRuleOptions;
}
export interface RRuleOptions {
freq: Frequency;
interval: number;
until?: Date;
count?: number;
byweekday?: Weekday[];
bymonthday?: number;
byyearday?: number;
byweekno?: number;
bymonth?: Months;
bysetpos?: number;
wkst?: Weekday;
bysecond?: number[];
byminute?: number[];
byday?: number[];
export type RRuleOptions = ICAL.RecurData;
}
enum Frequency {
YEARLY,
MONTHLY,
WEEKLY,
DAILY,
}
enum Ends {
Never,
Date,
After,
}
enum Months {
Jan = 1,
Feb,
@ -49,45 +25,64 @@ enum Months {
Nov,
Dec,
}
enum MonthRepeat {
Bysetpos,
Bymonthday,
}
enum Weekday {
enum WeekDay {
Su = 1,
Mo,
Tu,
We,
Th,
Fr,
Sa,
Su
}
const menuItemsMonths = Object.keys(Months).filter((key) => Number(key)).map((key) => {
const disableComplex = true;
const weekdays: WeekDay[] = Array.from(Array(7)).map((_, i) => i + 1);
const months: Months[] = Array.from(Array(12)).map((_, i) => i + 1);
const menuItemsEnds = [Ends.Never, Ends.Date, Ends.After].map((key) => {
return (
<MenuItem key={key} value={key}>{Months[key]}</MenuItem>
<MenuItem key={key} value={key}>{Ends[key]}</MenuItem>
);
});
const menuItemsEnds = [Ends.Never, Ends.Date, Ends.After].map((key) => {
const menuItemsFrequency = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY'].map((value) => {
return (
<MenuItem key={key} value={key}>{Ends[key]}</MenuItem>
<MenuItem key={value} value={value}>{value.toLowerCase()}</MenuItem>
);
});
const weekdays = [Weekday.Mo, Weekday.Tu, Weekday.We, Weekday.Th, Weekday.Fr, Weekday.Sa, Weekday.Su];
const menuItemsFrequency = [Frequency.YEARLY, Frequency.MONTHLY, Frequency.WEEKLY, Frequency.DAILY].map((value) => {
const menuItemMonths = months.map((month) => {
return (
<MenuItem key={value} value={value}>{Frequency[value]}</MenuItem>
<MenuItem key={month} value={month}>{Months[month]}</MenuItem>
);
});
const menuItemsWeekDays = weekdays.map((day) => {
return (
<MenuItem key={day} value={WeekDay[day].toUpperCase()}>{WeekDay[day]}</MenuItem>
);
});
export default function RRuleEteSync(props: PropsType) {
const options = props.rrule;
const styles = {
multiSelect: { minWidth: 120, maxWidth: '100%' },
width: { width: 120 },
};
interface PropsType {
onChange: (rrule: RRuleOptions) => void;
rrule: RRuleOptions;
}
export default function RRule(props: PropsType) {
const options = props.rrule;
function updateRule(newOptions: Partial<RRuleOptions>): void {
const updatedOptions = { ...options, ...newOptions };
props.onChange(updatedOptions);
}
function getEnds(): Ends {
if (options.until && !options.count) {
return Ends.Date;
@ -97,63 +92,24 @@ export default function RRuleEteSync(props: PropsType) {
return Ends.Never;
}
}
function handleCheckboxWeekday(event: React.FormEvent<{ value: unknown }>): void {
const checkbox = event.target as HTMLInputElement;
const weekday = Number(checkbox.value);
let byweekdayArray = options.byweekday as Weekday[];
let byweekday;
if (!checkbox.checked && byweekdayArray) {
byweekday = byweekdayArray.filter((day) => day !== weekday);
} else if (byweekdayArray) {
byweekdayArray = byweekdayArray.filter((day) => day !== weekday);
byweekday = [...byweekdayArray, weekday];
} else {
byweekday = [weekday];
}
updateRule({ byweekday: byweekday });
}
function isWeekdayChecked(day: number): boolean {
const weekdayArray = options.byweekday;
if (weekdayArray) {
return isNumber(weekdayArray.find((value) => Weekday[value] === Weekday[day]));
} else {
return false;
}
}
const checkboxWeekDays = weekdays.map((_, index) => {
return (
<FormControlLabel
control={
<Checkbox
key={index}
value={index}
checked={isWeekdayChecked(index)}
onChange={handleCheckboxWeekday}
/>}
key={index}
label={Weekday[index]} />
);
});
return (
<>
<div style={{ display: 'flex' }}>
<FormControlLabel
style={{ marginRight: 0 }}
value={options.freq}
label="Repeat every :"
label="Repeat every"
labelPlacement="start"
control={<TextField
style={{ marginLeft: '0.5em', width: '4em' }}
type="number"
inputProps={{ min: 1, max: 1000 }}
value={options.interval}
value={options.interval ?? 1}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
event.preventDefault();
const inputNode = event.currentTarget as HTMLInputElement;
if (inputNode.value === '') {
updateRule({ interval: undefined });
updateRule({ interval: 1 });
} else if (inputNode.valueAsNumber) {
updateRule({ interval: inputNode.valueAsNumber });
}
@ -162,70 +118,118 @@ export default function RRuleEteSync(props: PropsType) {
/>
<Select
value={options.freq}
style={{ alignSelf: 'flex-end', marginLeft: 20 }}
style={{ marginLeft: '0.5em' }}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
const freq = Number((event.target as HTMLSelectElement).value);
const updatedOptions = {
freq: freq,
bysetpos: undefined,
bymonthday: freq === Frequency.MONTHLY || Frequency.YEARLY === freq ? 1 : undefined,
byweekday: undefined,
bymonth: freq === Frequency.YEARLY ? Months.Jan : undefined,
};
updateRule(updatedOptions);
const freq = (event.target as HTMLSelectElement).value as ICAL.FrequencyValues;
updateRule({ freq: freq });
}}
>
{menuItemsFrequency}
</Select>
</div>
<div style={{ display: 'flex' }}>
{(options.freq === Frequency.MONTHLY) &&
<Select
value={options.bysetpos ? MonthRepeat.Bysetpos : MonthRepeat.Bymonthday}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
const value = Number((event.target as HTMLInputElement).value);
if (value === MonthRepeat.Bymonthday) {
updateRule({ bymonthday: 1, bysetpos: undefined, bymonth: Months.Jan });
} else if (value === MonthRepeat.Bysetpos) {
updateRule({ bysetpos: 1, bymonthday: undefined, bymonth: undefined });
}
}}
>
<MenuItem value={MonthRepeat.Bymonthday}>On</MenuItem>
<MenuItem value={MonthRepeat.Bysetpos}>On the</MenuItem>
</Select>
}
{options.bysetpos &&
<Select
value={options.bysetpos}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
updateRule({ bysetpos: Number((event.target as HTMLInputElement).value) });
}}>
<MenuItem value={1}>First</MenuItem>
<MenuItem value={2}>Second</MenuItem>
<MenuItem value={3}>Third</MenuItem>
<MenuItem value={4}>Fourth</MenuItem>
<MenuItem value={-1}>Last</MenuItem>
</Select>
}
{(options.freq === Frequency.YEARLY && options.bymonth) &&
<Select
value={options.bymonth}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
updateRule({ bymonth: Number((event.target as HTMLInputElement).value) });
}}>
{menuItemsMonths}
</Select>
{!disableComplex && (
<div style={{ display: 'flex' }}>
{(options.freq === 'MONTHLY') &&
<Select value={options.bysetpos ? MonthRepeat.Bysetpos : MonthRepeat.Bymonthday}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
const value = Number((event.target as HTMLInputElement).value);
if (value === MonthRepeat.Bymonthday) {
updateRule({ bymonthday: [1], bysetpos: undefined, bymonth: [Months.Jan] });
} else if (value === MonthRepeat.Bysetpos) {
updateRule({ bysetpos: [1], bymonthday: undefined, bymonth: undefined });
}
}}>
<MenuItem value={MonthRepeat.Bymonthday}>On</MenuItem>
<MenuItem value={MonthRepeat.Bysetpos}>On the</MenuItem>
</Select>
}
{options.bysetpos &&
<Select value={options.bysetpos[0]}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
updateRule({ bysetpos: [Number((event.target as HTMLInputElement).value)] });
}}>
<MenuItem value={1}>First</MenuItem>
<MenuItem value={2}>Second</MenuItem>
<MenuItem value={3}>Third</MenuItem>
<MenuItem value={4}>Fourth</MenuItem>
<MenuItem value={-1}>Last</MenuItem>
</Select>
}
</div>
)}
<FormControl>
<InputLabel>Ends</InputLabel>
<Select
value={getEnds()}
style={styles.width}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
const value = Number((event.target as HTMLSelectElement).value);
let updateOptions;
if (value === Ends.Date) {
updateOptions = { count: undefined, until: ICAL.Time.now() };
} else if (value === Ends.After) {
updateOptions = { until: undefined, count: 1 };
} else {
updateOptions = { count: undefined, until: undefined };
}
updateRule(updateOptions);
}}>
{menuItemsEnds}
</Select>
</FormControl>
{options.until &&
<DateTimePicker
dateOnly
value={options.until?.toJSDate() || undefined}
placeholder="Ends"
onChange={(date?: Date) => {
const value = date ? date : null;
updateRule({ until: ICAL.Time.fromJSDate(value, true) });
}}
/>
}
{options.count &&
<TextField
type="number"
value={options.count}
label="Count"
style={{ width: 60 }}
inputProps={{ min: 1, step: 1 }}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
event.preventDefault();
const inputNode = event.currentTarget as HTMLInputElement;
if (inputNode.value === '') {
updateRule({ count: 1 });
} else if (inputNode.valueAsNumber) {
updateRule({ count: inputNode.valueAsNumber });
}
}}
/>
}
<div>
{(options.freq && options.freq !== 'DAILY') &&
<div>
<FormControl>
<InputLabel>Weekdays</InputLabel>
<Select
value={options.byday ? options.byday : []}
multiple
style={styles.multiSelect}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
const value = event.target.value as string[];
updateRule({ byday: value });
}}>
{menuItemsWeekDays}
</Select>
</FormControl>
</div>
}
{options.bymonthday &&
{options.freq === 'MONTHLY' &&
<TextField
type="number"
value={options.bymonthday}
value={options.bymonthday ? options.bymonthday[0] : undefined}
label="Month day"
style={{ width: 100 }}
style={styles.width}
inputProps={{ min: 1, step: 1, max: 31 }}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
event.preventDefault();
@ -234,61 +238,27 @@ export default function RRuleEteSync(props: PropsType) {
if (value === '') {
updateRule({ bymonthday: undefined });
} else if (numberValue < 32 && numberValue > 0) {
updateRule({ bymonthday: numberValue });
updateRule({ bymonthday: [numberValue] });
}
}}
/>
}
</div>
<div>
{options.freq !== Frequency.DAILY &&
<FormGroup row>{checkboxWeekDays}</FormGroup>
}
<FormControl>
<InputLabel>Ends</InputLabel>
<Select
value={getEnds()}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
const value = Number((event.target as HTMLSelectElement).value);
let updateOptions;
if (value === Ends.Date) {
updateOptions = { count: undefined, until: new Date() };
} else if (value === Ends.After) {
updateOptions = { until: undefined, count: 1 };
} else {
updateOptions = { count: undefined, until: undefined };
}
updateRule(updateOptions);
}}>
{menuItemsEnds}
</Select>
</FormControl>
{options.until &&
<DateTimePicker
dateOnly
value={options.until || undefined}
placeholder="Ends"
onChange={(date?: Date) => updateRule({ until: date })}
/>
}
{options.count &&
<TextField
type="number"
value={options.count}
label="Count"
style={{ width: 60 }}
inputProps={{ min: 1, step: 1 }}
onChange={(event: React.FormEvent<{ value: unknown }>) => {
event.preventDefault();
const inputNode = event.currentTarget as HTMLInputElement;
if (inputNode.value === '') {
updateRule({ count: 1 });
} else if (inputNode.valueAsNumber) {
updateRule({ count: inputNode.valueAsNumber });
}
}}
/>
{options.freq === 'YEARLY' &&
<div>
<FormControl>
<InputLabel>Months</InputLabel>
<Select
style={styles.multiSelect}
value={options.bymonth ? options.bymonth : []}
multiple
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
const value = event.target.value as string[];
updateRule({ bymonth: value.map((month) => Number(month)) });
}}>
{menuItemMonths}
</Select>
</FormControl>
</div>
}
</div>
</>

Loading…
Cancel
Save