Removes static run on string options and opts for the more dynamic ux pattern already adopted in the old UI

This commit is contained in:
mabashian 2020-03-30 15:36:48 -04:00
parent b4ea60eb79
commit c7b23aac9b
8 changed files with 403 additions and 429 deletions

View File

@ -25,7 +25,16 @@ class AnsibleSelect extends React.Component {
}
render() {
const { id, data, i18n, isValid, onBlur, value, className } = this.props;
const {
id,
data,
i18n,
isValid,
onBlur,
value,
className,
isDisabled,
} = this.props;
return (
<FormSelect
@ -36,6 +45,7 @@ class AnsibleSelect extends React.Component {
aria-label={i18n._(t`Select Input`)}
isValid={isValid}
className={className}
isDisabled={isDisabled}
>
{data.map(option => (
<FormSelectOption
@ -62,6 +72,7 @@ AnsibleSelect.defaultProps = {
isValid: true,
onBlur: () => {},
className: '',
isDisabled: false,
};
AnsibleSelect.propTypes = {
@ -72,6 +83,7 @@ AnsibleSelect.propTypes = {
onChange: func.isRequired,
value: oneOfType([string, number]).isRequired,
className: string,
isDisabled: bool,
};
export { AnsibleSelect as _AnsibleSelect };

View File

@ -6,19 +6,9 @@ import { useHistory, useLocation } from 'react-router-dom';
import { RRule } from 'rrule';
import { Card } from '@patternfly/react-core';
import { CardBody } from '@components/Card';
import { getWeekNumber } from '@util/dates';
import { getRRuleDayConstants } from '@util/dates';
import ScheduleForm from '../shared/ScheduleForm';
const days = {
0: 'SU',
1: 'MO',
2: 'TU',
3: 'WE',
4: 'TH',
5: 'FR',
6: 'SA',
};
function ScheduleAdd({ i18n, createSchedule }) {
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
@ -71,31 +61,22 @@ function ScheduleAdd({ i18n, createSchedule }) {
break;
case 'month':
ruleObj.freq = RRule.MONTHLY;
if (values.runOn === 'number') {
ruleObj.bymonthday = startDay;
} else if (values.runOn === 'day') {
ruleObj.byweekday =
RRule[days[new Date(values.startDateTime).getDay()]];
ruleObj.bysetpos = getWeekNumber(values.startDateTime);
} else if (values.runOn === 'lastDay') {
ruleObj.byweekday =
RRule[days[new Date(values.startDateTime).getDay()]];
ruleObj.bysetpos = -1;
if (values.runOn === 'day') {
ruleObj.bymonthday = values.runOnDayNumber;
} else if (values.runOn === 'the') {
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay, i18n);
}
break;
case 'year':
ruleObj.freq = RRule.YEARLY;
ruleObj.bymonth = new Date(values.startDateTime).getMonth() + 1;
if (values.runOn === 'number') {
ruleObj.bymonthday = startDay;
} else if (values.runOn === 'day') {
ruleObj.byweekday =
RRule[days[new Date(values.startDateTime).getDay()]];
ruleObj.bysetpos = getWeekNumber(values.startDateTime);
} else if (values.runOn === 'lastDay') {
ruleObj.byweekday =
RRule[days[new Date(values.startDateTime).getDay()]];
ruleObj.bysetpos = -1;
if (values.runOn === 'day') {
ruleObj.bymonth = parseInt(values.runOnDayMonth, 10);
ruleObj.bymonthday = values.runOnDayNumber;
} else if (values.runOn === 'the') {
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay, i18n);
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
}
break;
default:

View File

@ -64,7 +64,6 @@ describe('<ScheduleAdd />', () => {
interval: 10,
name: 'Run every 10 minutes 10 times',
occurrences: 10,
runOn: 'number',
startDateTime: '2020-03-25T10:30:00',
timezone: 'America/New_York',
});
@ -85,7 +84,6 @@ describe('<ScheduleAdd />', () => {
frequency: 'hour',
interval: 1,
name: 'Run every hour until date',
runOn: 'number',
startDateTime: '2020-03-25T10:45:00',
timezone: 'America/New_York',
});
@ -105,7 +103,6 @@ describe('<ScheduleAdd />', () => {
frequency: 'day',
interval: 1,
name: 'Run daily',
runOn: 'number',
startDateTime: '2020-03-25T10:45:00',
timezone: 'America/New_York',
});
@ -127,7 +124,6 @@ describe('<ScheduleAdd />', () => {
interval: 1,
name: 'Run weekly on mon/wed/fri',
occurrences: 1,
runOn: 'number',
startDateTime: '2020-03-25T10:45:00',
timezone: 'America/New_York',
});
@ -148,7 +144,8 @@ describe('<ScheduleAdd />', () => {
interval: 1,
name: 'Run on the first day of the month',
occurrences: 1,
runOn: 'number',
runOn: 'day',
runOnDayNumber: 1,
startDateTime: '2020-04-01T10:45',
timezone: 'America/New_York',
});
@ -157,7 +154,7 @@ describe('<ScheduleAdd />', () => {
description: 'test description',
name: 'Run on the first day of the month',
rrule:
'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=01',
'DTSTART;TZID=America/New_York:20200401T104500 RRULE:INTERVAL=1;FREQ=MONTHLY;BYMONTHDAY=1',
});
});
test('Successfully creates a schedule with monthly repeat frequency on the last tuesday of the month', async () => {
@ -170,7 +167,9 @@ describe('<ScheduleAdd />', () => {
interval: 1,
name: 'Run monthly on the last Tuesday',
occurrences: 1,
runOn: 'lastDay',
runOn: 'the',
runOnTheDay: 'tuesday',
runOnTheOccurrence: -1,
startDateTime: '2020-03-31T11:00',
timezone: 'America/New_York',
});
@ -179,7 +178,7 @@ describe('<ScheduleAdd />', () => {
description: 'test description',
name: 'Run monthly on the last Tuesday',
rrule:
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYDAY=TU;BYSETPOS=-1',
'DTSTART;TZID=America/New_York:20200331T110000 RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=-1;BYDAY=TU',
});
});
test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
@ -191,7 +190,9 @@ describe('<ScheduleAdd />', () => {
interval: 1,
name: 'Yearly on the first day of March',
occurrences: 1,
runOn: 'number',
runOn: 'day',
runOnDayMonth: 3,
runOnDayNumber: 1,
startDateTime: '2020-03-01T00:00',
timezone: 'America/New_York',
});
@ -200,7 +201,7 @@ describe('<ScheduleAdd />', () => {
description: 'test description',
name: 'Yearly on the first day of March',
rrule:
'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=01',
'DTSTART;TZID=America/New_York:20200301T000000 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=1',
});
});
test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
@ -212,7 +213,10 @@ describe('<ScheduleAdd />', () => {
interval: 1,
name: 'Yearly on the second Friday in April',
occurrences: 1,
runOn: 'day',
runOn: 'the',
runOnTheOccurrence: 2,
runOnTheDay: 'friday',
runOnTheMonth: 4,
startDateTime: '2020-04-10T11:15',
timezone: 'America/New_York',
});
@ -221,7 +225,31 @@ describe('<ScheduleAdd />', () => {
description: 'test description',
name: 'Yearly on the second Friday in April',
rrule:
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=4;BYDAY=FR;BYSETPOS=2',
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=2;BYDAY=FR;BYMONTH=4',
});
});
test('Successfully creates a schedule with yearly repeat frequency on the first weekday in October', async () => {
await act(async () => {
wrapper.find('ScheduleForm').invoke('handleSubmit')({
description: 'test description',
end: 'never',
frequency: 'year',
interval: 1,
name: 'Yearly on the first weekday in October',
occurrences: 1,
runOn: 'the',
runOnTheOccurrence: 1,
runOnTheDay: 'weekday',
runOnTheMonth: 10,
startDateTime: '2020-04-10T11:15',
timezone: 'America/New_York',
});
});
expect(createSchedule).toHaveBeenCalledWith({
description: 'test description',
name: 'Yearly on the first weekday in October',
rrule:
'DTSTART;TZID=America/New_York:20200410T111500 RRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=1;BYDAY=MO,TU,WE,TH,FR;BYMONTH=10',
});
});
});

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React from 'react';
import styled from 'styled-components';
import { useField } from 'formik';
import { withI18n } from '@lingui/react';
@ -9,16 +9,25 @@ import {
Radio,
TextInput,
} from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect';
import FormField from '@components/FormField';
import {
getDaysInMonth,
getDayString,
getMonthString,
getWeekString,
getWeekNumber,
} from '@util/dates';
import { required } from '@util/validators';
const RunOnRadio = styled(Radio)`
label {
display: block;
width: 100%;
}
:not(:last-of-type) {
margin-bottom: 10px;
}
select:not(:first-of-type) {
margin-left: 10px;
}
`;
const RunEveryLabel = styled.p`
display: flex;
align-items: center;
@ -48,6 +57,21 @@ export function requiredPositiveInteger(i18n) {
}
const FrequencyDetailSubform = ({ i18n }) => {
const [runOnDayMonth] = useField({
name: 'runOnDayMonth',
});
const [runOnDayNumber] = useField({
name: 'runOnDayNumber',
});
const [runOnTheOccurrence] = useField({
name: 'runOnTheOccurrence',
});
const [runOnTheDay] = useField({
name: 'runOnTheDay',
});
const [runOnTheMonth] = useField({
name: 'runOnTheMonth',
});
const [startDateTime] = useField({
name: 'startDateTime',
});
@ -63,7 +87,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
name: 'interval',
validate: requiredPositiveInteger(i18n),
});
const [runOn, runOnMeta, runOnHelpers] = useField({
const [runOn, runOnMeta] = useField({
name: 'runOn',
validate: required(i18n._(t`Select a value for this field`), i18n),
});
@ -79,20 +103,64 @@ const FrequencyDetailSubform = ({ i18n }) => {
validate: requiredPositiveInteger(i18n),
});
useEffect(() => {
// The Last day option disappears if the start date isn't in the
// last week of the month. If that value was selected when this
// happens then we'll clear out the selection and force the user
// to choose between the remaining two.
if (
(frequency.value === 'month' || frequency.value === 'year') &&
runOn.value === 'lastDay' &&
getDaysInMonth(startDateTime.value) - 7 >=
new Date(startDateTime.value).getDate()
) {
runOnHelpers.setValue('');
}
}, [startDateTime.value, frequency.value, runOn.value, runOnHelpers]);
const monthOptions = [
{
key: 'january',
value: 1,
label: i18n._(t`January`),
},
{
key: 'february',
value: 2,
label: i18n._(t`February`),
},
{
key: 'march',
value: 3,
label: i18n._(t`March`),
},
{
key: 'april',
value: 4,
label: i18n._(t`April`),
},
{
key: 'may',
value: 5,
label: i18n._(t`May`),
},
{
key: 'june',
value: 6,
label: i18n._(t`June`),
},
{
key: 'july',
value: 7,
label: i18n._(t`July`),
},
{ key: 'august', value: 8, label: i18n._(t`August`) },
{
key: 'september',
value: 9,
label: i18n._(t`September`),
},
{
key: 'october',
value: 10,
label: i18n._(t`October`),
},
{
key: 'november',
value: 11,
label: i18n._(t`November`),
},
{
key: 'december',
value: 12,
label: i18n._(t`December`),
},
];
const updateDaysOfWeek = (day, checked) => {
const newDaysOfWeek = [...daysOfWeek.value];
@ -149,66 +217,6 @@ const FrequencyDetailSubform = ({ i18n }) => {
}
};
const generateRunOnNumberLabel = () => {
switch (frequency.value) {
case 'month':
return i18n._(
t`Day ${startDateTime.value.split('T')[0].split('-')[2]}`
);
case 'year': {
const monthString = getMonthString(
new Date(startDateTime.value).getMonth(),
i18n
);
return `${monthString} ${new Date(startDateTime.value).getDate()}`;
}
default:
throw new Error(i18n._(t`Frequency did not match an expected value`));
}
};
const generateRunOnDayLabel = () => {
const dayString = getDayString(
new Date(startDateTime.value).getDay(),
i18n
);
const weekNumber = getWeekNumber(startDateTime.value);
const weekString = getWeekString(weekNumber, i18n);
switch (frequency.value) {
case 'month':
return i18n._(t`The ${weekString} ${dayString}`);
case 'year': {
const monthString = getMonthString(
new Date(startDateTime.value).getMonth(),
i18n
);
return i18n._(t`The ${weekString} ${dayString} in ${monthString}`);
}
default:
throw new Error(i18n._(t`Frequency did not match an expected value`));
}
};
const generateRunOnLastDayLabel = () => {
const dayString = getDayString(
new Date(startDateTime.value).getDay(),
i18n
);
switch (frequency.value) {
case 'month':
return i18n._(t`The last ${dayString}`);
case 'year': {
const monthString = getMonthString(
new Date(startDateTime.value).getMonth(),
i18n
);
return i18n._(t`The last ${dayString} in ${monthString}`);
}
default:
throw new Error(i18n._(t`Frequency did not match an expected value`));
}
};
/* eslint-disable no-restricted-globals */
return (
<>
@ -252,7 +260,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('SU', checked);
}}
aria-label={i18n._(t`Sunday`)}
id="days-of-week-sun"
id="schedule-days-of-week-sun"
name="daysOfWeek"
/>
<Checkbox
@ -262,7 +270,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('MO', checked);
}}
aria-label={i18n._(t`Monday`)}
id="days-of-week-mon"
id="schedule-days-of-week-mon"
name="daysOfWeek"
/>
<Checkbox
@ -272,7 +280,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('TU', checked);
}}
aria-label={i18n._(t`Tuesday`)}
id="days-of-week-tue"
id="schedule-days-of-week-tue"
name="daysOfWeek"
/>
<Checkbox
@ -282,7 +290,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('WE', checked);
}}
aria-label={i18n._(t`Wednesday`)}
id="days-of-week-wed"
id="schedule-days-of-week-wed"
name="daysOfWeek"
/>
<Checkbox
@ -292,7 +300,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('TH', checked);
}}
aria-label={i18n._(t`Thursday`)}
id="days-of-week-thu"
id="schedule-days-of-week-thu"
name="daysOfWeek"
/>
<Checkbox
@ -302,7 +310,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('FR', checked);
}}
aria-label={i18n._(t`Friday`)}
id="days-of-week-fri"
id="schedule-days-of-week-fri"
name="daysOfWeek"
/>
<Checkbox
@ -312,7 +320,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('SA', checked);
}}
aria-label={i18n._(t`Saturday`)}
id="days-of-week-sat"
id="schedule-days-of-week-sat"
name="daysOfWeek"
/>
</div>
@ -328,21 +336,39 @@ const FrequencyDetailSubform = ({ i18n }) => {
isValid={!runOnMeta.touched || !runOnMeta.error}
label={i18n._(t`Run on`)}
>
<Radio
id="run-on-number"
<RunOnRadio
id="schedule-run-on-day"
name="runOn"
label={generateRunOnNumberLabel()}
value="number"
isChecked={runOn.value === 'number'}
onChange={(value, event) => {
event.target.value = 'number';
runOn.onChange(event);
}}
/>
<Radio
id="run-on-day"
name="runOn"
label={generateRunOnDayLabel()}
label={
<div css="display: flex;align-items: center;">
{frequency?.value === 'month' && (
<span id="foobar" css="margin-right: 10px;">
Day
</span>
)}
{frequency?.value === 'year' && (
<AnsibleSelect
id="schedule-run-on-day-month"
css="margin-right: 10px"
isDisabled={runOn.value !== 'day'}
data={monthOptions}
{...runOnDayMonth}
/>
)}
<TextInput
id="schedule-run-on-day-number"
type="number"
min="1"
max="31"
step="1"
isDisabled={runOn.value !== 'day'}
{...runOnDayNumber}
onChange={(value, event) => {
runOnDayNumber.onChange(event);
}}
/>
</div>
}
value="day"
isChecked={runOn.value === 'day'}
onChange={(value, event) => {
@ -350,20 +376,105 @@ const FrequencyDetailSubform = ({ i18n }) => {
runOn.onChange(event);
}}
/>
{new Date(startDateTime.value).getDate() >
getDaysInMonth(startDateTime.value) - 7 && (
<Radio
id="run-on-last-day"
name="runOn"
label={generateRunOnLastDayLabel()}
value="lastDay"
isChecked={runOn.value === 'lastDay'}
onChange={(value, event) => {
event.target.value = 'lastDay';
runOn.onChange(event);
}}
/>
)}
<RunOnRadio
id="schedule-run-on-the"
name="runOn"
label={
<div css="display: flex;align-items: center;">
<span id="foobar" css="margin-right: 10px;">
The
</span>
<AnsibleSelect
id="schedule-run-on-the-occurrence"
isDisabled={runOn.value !== 'the'}
data={[
{ value: 1, key: 'first', label: i18n._(t`First`) },
{
value: 2,
key: 'second',
label: i18n._(t`Second`),
},
{ value: 3, key: 'third', label: i18n._(t`Third`) },
{
value: 4,
key: 'fourth',
label: i18n._(t`Fourth`),
},
{ value: 5, key: 'fifth', label: i18n._(t`Fifth`) },
{ value: -1, key: 'last', label: i18n._(t`Last`) },
]}
{...runOnTheOccurrence}
/>
<AnsibleSelect
id="schedule-run-on-the-day"
isDisabled={runOn.value !== 'the'}
data={[
{
value: 'sunday',
key: 'sunday',
label: i18n._(t`Sunday`),
},
{
value: 'monday',
key: 'monday',
label: i18n._(t`Monday`),
},
{
value: 'tuesday',
key: 'tuesday',
label: i18n._(t`Tuesday`),
},
{
value: 'wednesday',
key: 'wednesday',
label: i18n._(t`Wednesday`),
},
{
value: 'thursday',
key: 'thursday',
label: i18n._(t`Thursday`),
},
{
value: 'friday',
key: 'friday',
label: i18n._(t`Friday`),
},
{
value: 'saturday',
key: 'saturday',
label: i18n._(t`Saturday`),
},
{ value: 'day', key: 'day', label: i18n._(t`Day`) },
{
value: 'weekday',
key: 'weekday',
label: i18n._(t`Weekday`),
},
{
value: 'weekendDay',
key: 'weekendDay',
label: i18n._(t`Weekend day`),
},
]}
{...runOnTheDay}
/>
{frequency?.value === 'year' && (
<AnsibleSelect
id="schedule-run-on-the-month"
isDisabled={runOn.value !== 'the'}
data={monthOptions}
{...runOnTheMonth}
/>
)}
</div>
}
value="the"
isChecked={runOn.value === 'the'}
onChange={(value, event) => {
event.target.value = 'the';
runOn.onChange(event);
}}
/>
</FormGroup>
)}
<FormGroup

View File

@ -172,14 +172,26 @@ function ScheduleForm({
interval: 1,
name: schedule.name || '',
occurrences: 1,
runOn: 'number',
runOn: 'day',
runOnDayMonth: 1,
runOnDayNumber: 1,
runOnTheDay: 'sunday',
runOnTheMonth: 1,
runOnTheOccurrence: 1,
startDateTime: dateToInputDateTime(closestQuarterHour),
timezone: schedule.timezone || 'America/New_York',
}}
onSubmit={handleSubmit}
validate={values => {
const errors = {};
const { end, endDateTime, startDateTime } = values;
const {
end,
endDateTime,
frequency,
runOn,
runOnDayNumber,
startDateTime,
} = values;
if (
end === 'onDate' &&
@ -190,6 +202,16 @@ function ScheduleForm({
);
}
if (
(frequency === 'month' || frequency === 'year') &&
runOn === 'day' &&
(runOnDayNumber < 1 || runOnDayNumber > 31)
) {
errors.runOn = i18n._(
t`Please select a day number between 1 and 31`
);
}
return errors;
}}
>

View File

@ -216,67 +216,17 @@ describe('<ScheduleForm />', () => {
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
});
test('month run on options displayed correctly as date changes', async () => {
await act(async () => {
wrapper.find('input#schedule-start-datetime').simulate('change', {
target: { value: '2020-03-23T01:45:00', name: 'startDateTime' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 23');
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day + label').text()).toBe(
'The fourth Monday'
expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
true
);
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
await act(async () => {
wrapper.find('input#schedule-start-datetime').simulate('change', {
target: { value: '2020-03-27T01:45:00', name: 'startDateTime' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 27');
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day + label').text()).toBe(
'The fourth Friday'
expect(
wrapper.find('input#schedule-run-on-day-number').prop('value')
).toBe(1);
expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
false
);
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-last-day + label').text()).toBe(
'The last Friday'
);
});
test('month run on cleared when last day selected but date changes from one of the last seven days of the month', async () => {
await act(async () => {
wrapper.find('Radio#run-on-last-day').invoke('onChange')('lastDay', {
target: { name: 'runOn' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(true);
await act(async () => {
wrapper.find('input#schedule-start-datetime').simulate('change', {
target: { value: '2020-03-15T01:45:00', name: 'startDateTime' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-number + label').text()).toBe('Day 15');
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day + label').text()).toBe(
'The third Sunday'
);
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
await act(async () => {
wrapper.find('Radio#run-on-number').invoke('onChange')('number', {
target: { name: 'runOn' },
});
});
wrapper.update();
expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(0);
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(0);
});
test('correct frequency details fields and values shown when frequency changed to year', async () => {
const runFrequencySelect = wrapper.find(
@ -300,43 +250,19 @@ describe('<ScheduleForm />', () => {
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);
expect(wrapper.find('input#end-on-date').prop('checked')).toBe(false);
expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
true
);
expect(
wrapper.find('input#schedule-run-on-day-number').prop('value')
).toBe(1);
expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
false
);
expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(1);
expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(1);
});
test('year run on options displayed correctly as date changes', async () => {
await act(async () => {
wrapper.find('input#schedule-start-datetime').simulate('change', {
target: { value: '2020-03-23T01:45:00', name: 'startDateTime' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
expect(wrapper.find('input#run-on-number + label').text()).toBe(
'March 23'
);
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day + label').text()).toBe(
'The fourth Monday in March'
);
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
await act(async () => {
wrapper.find('input#schedule-start-datetime').simulate('change', {
target: { value: '2020-03-27T01:45:00', name: 'startDateTime' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(true);
expect(wrapper.find('input#run-on-number + label').text()).toBe(
'March 27'
);
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day + label').text()).toBe(
'The fourth Friday in March'
);
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-last-day + label').text()).toBe(
'The last Friday in March'
);
});
test('occurrences field properly shown when that run on selection is made', async () => {
test('occurrences field properly shown when end after selection is made', async () => {
await act(async () => {
wrapper.find('Radio#end-after').invoke('onChange')('after', {
target: { name: 'end' },
@ -355,32 +281,6 @@ describe('<ScheduleForm />', () => {
});
wrapper.update();
});
test('year run on cleared when last day selected but date changes from one of the last seven days of the month', async () => {
await act(async () => {
wrapper.find('Radio#run-on-last-day').invoke('onChange')('lastDay', {
target: { name: 'runOn' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-last-day').prop('checked')).toBe(true);
await act(async () => {
wrapper.find('input#schedule-start-datetime').simulate('change', {
target: { value: '2020-03-15T01:45:00', name: 'startDateTime' },
});
});
wrapper.update();
expect(wrapper.find('input#run-on-number').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-number + label').text()).toBe(
'March 15'
);
expect(wrapper.find('input#run-on-day').prop('checked')).toBe(false);
expect(wrapper.find('input#run-on-day + label').text()).toBe(
'The third Sunday in March'
);
expect(wrapper.find('input#run-on-last-day').length).toBe(0);
});
test('error shown when end date/time comes before start date/time', async () => {
expect(wrapper.find('input#end-never').prop('checked')).toBe(true);
expect(wrapper.find('input#end-after').prop('checked')).toBe(false);

View File

@ -1,5 +1,6 @@
/* eslint-disable import/prefer-default-export */
import { t } from '@lingui/macro';
import { RRule } from 'rrule';
import { getLanguage } from './language';
const prependZeros = value => value.toString().padStart(2, 0);
@ -28,87 +29,37 @@ export function dateToInputDateTime(dateObj) {
return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
}
export function getDaysInMonth(dateString) {
const dateObj = new Date(dateString);
return new Date(dateObj.getFullYear(), dateObj.getMonth() + 1, 0).getDate();
}
export function getWeekNumber(dateString) {
const dateObj = new Date(dateString);
const dayOfMonth = dateObj.getDate();
const dayOfWeek = dateObj.getDay();
if (dayOfMonth < 8) {
return 1;
}
dateObj.setDate(dayOfMonth - dayOfWeek + 1);
return Math.ceil(dayOfMonth / 7);
}
export function getDayString(dayIndex, i18n) {
switch (dayIndex) {
case 0:
return i18n._(t`Sunday`);
case 1:
return i18n._(t`Monday`);
case 2:
return i18n._(t`Tuesday`);
case 3:
return i18n._(t`Wednesday`);
case 4:
return i18n._(t`Thursday`);
case 5:
return i18n._(t`Friday`);
case 6:
return i18n._(t`Saturday`);
export function getRRuleDayConstants(dayString, i18n) {
switch (dayString) {
case 'sunday':
return RRule.SU;
case 'monday':
return RRule.MO;
case 'tuesday':
return RRule.TU;
case 'wednesday':
return RRule.WE;
case 'thursday':
return RRule.TH;
case 'friday':
return RRule.FR;
case 'saturday':
return RRule.SA;
case 'day':
return [
RRule.MO,
RRule.TU,
RRule.WE,
RRule.TH,
RRule.FR,
RRule.SA,
RRule.SU,
];
case 'weekday':
return [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR];
case 'weekendDay':
return [RRule.SA, RRule.SU];
default:
throw new Error(i18n._(t`Unrecognized day index`));
}
}
export function getWeekString(weekNumber, i18n) {
switch (weekNumber) {
case 1:
return i18n._(t`first`);
case 2:
return i18n._(t`second`);
case 3:
return i18n._(t`third`);
case 4:
return i18n._(t`fourth`);
case 5:
return i18n._(t`fifth`);
default:
throw new Error(i18n._(t`Unrecognized week number`));
}
}
export function getMonthString(monthIndex, i18n) {
switch (monthIndex) {
case 0:
return i18n._(t`January`);
case 1:
return i18n._(t`February`);
case 2:
return i18n._(t`March`);
case 3:
return i18n._(t`April`);
case 4:
return i18n._(t`May`);
case 5:
return i18n._(t`June`);
case 6:
return i18n._(t`July`);
case 7:
return i18n._(t`August`);
case 8:
return i18n._(t`September`);
case 9:
return i18n._(t`October`);
case 10:
return i18n._(t`November`);
case 11:
return i18n._(t`December`);
default:
throw new Error(i18n._(t`Unrecognized month index`));
throw new Error(i18n._(t`Unrecognized day string`));
}
}

View File

@ -1,12 +1,9 @@
import { RRule } from 'rrule';
import {
dateToInputDateTime,
getDaysInMonth,
getDayString,
getMonthString,
getWeekNumber,
getWeekString,
formatDateString,
formatDateStringUTC,
getRRuleDayConstants,
secondsToHHMMSS,
} from './dates';
@ -59,63 +56,35 @@ describe('dateToInputDateTime', () => {
});
});
describe('getDaysInMonth', () => {
describe('getRRuleDayConstants', () => {
test('it returns the expected value', () => {
expect(getDaysInMonth('2020-02-15T00:00:00Z')).toEqual(29);
expect(getDaysInMonth('2020-03-15T00:00:00Z')).toEqual(31);
expect(getDaysInMonth('2020-04-15T00:00:00Z')).toEqual(30);
});
});
describe('getWeekNumber', () => {
test('it returns the expected value', () => {
expect(getWeekNumber('2020-02-01T00:00:00Z')).toEqual(1);
expect(getWeekNumber('2020-02-08T00:00:00Z')).toEqual(2);
expect(getWeekNumber('2020-02-15T00:00:00Z')).toEqual(3);
expect(getWeekNumber('2020-02-22T00:00:00Z')).toEqual(4);
expect(getWeekNumber('2020-02-29T00:00:00Z')).toEqual(5);
});
});
describe('getDayString', () => {
test('it returns the expected value', () => {
expect(getDayString(0, i18n)).toEqual('Sunday');
expect(getDayString(1, i18n)).toEqual('Monday');
expect(getDayString(2, i18n)).toEqual('Tuesday');
expect(getDayString(3, i18n)).toEqual('Wednesday');
expect(getDayString(4, i18n)).toEqual('Thursday');
expect(getDayString(5, i18n)).toEqual('Friday');
expect(getDayString(6, i18n)).toEqual('Saturday');
expect(() => getDayString(7, i18n)).toThrow();
});
});
describe('getWeekString', () => {
test('it returns the expected value', () => {
expect(() => getWeekString(0, i18n)).toThrow();
expect(getWeekString(1, i18n)).toEqual('first');
expect(getWeekString(2, i18n)).toEqual('second');
expect(getWeekString(3, i18n)).toEqual('third');
expect(getWeekString(4, i18n)).toEqual('fourth');
expect(getWeekString(5, i18n)).toEqual('fifth');
expect(() => getWeekString(6, i18n)).toThrow();
});
});
describe('getMonthString', () => {
test('it returns the expected value', () => {
expect(getMonthString(0, i18n)).toEqual('January');
expect(getMonthString(1, i18n)).toEqual('February');
expect(getMonthString(2, i18n)).toEqual('March');
expect(getMonthString(3, i18n)).toEqual('April');
expect(getMonthString(4, i18n)).toEqual('May');
expect(getMonthString(5, i18n)).toEqual('June');
expect(getMonthString(6, i18n)).toEqual('July');
expect(getMonthString(7, i18n)).toEqual('August');
expect(getMonthString(8, i18n)).toEqual('September');
expect(getMonthString(9, i18n)).toEqual('October');
expect(getMonthString(10, i18n)).toEqual('November');
expect(getMonthString(11, i18n)).toEqual('December');
expect(() => getMonthString(12, i18n)).toThrow();
expect(getRRuleDayConstants('monday', i18n)).toEqual(RRule.MO);
expect(getRRuleDayConstants('tuesday', i18n)).toEqual(RRule.TU);
expect(getRRuleDayConstants('wednesday', i18n)).toEqual(RRule.WE);
expect(getRRuleDayConstants('thursday', i18n)).toEqual(RRule.TH);
expect(getRRuleDayConstants('friday', i18n)).toEqual(RRule.FR);
expect(getRRuleDayConstants('saturday', i18n)).toEqual(RRule.SA);
expect(getRRuleDayConstants('sunday', i18n)).toEqual(RRule.SU);
expect(getRRuleDayConstants('day', i18n)).toEqual([
RRule.MO,
RRule.TU,
RRule.WE,
RRule.TH,
RRule.FR,
RRule.SA,
RRule.SU,
]);
expect(getRRuleDayConstants('weekday', i18n)).toEqual([
RRule.MO,
RRule.TU,
RRule.WE,
RRule.TH,
RRule.FR,
]);
expect(getRRuleDayConstants('weekendDay', i18n)).toEqual([
RRule.SA,
RRule.SU,
]);
expect(() => getRRuleDayConstants('foobar', i18n)).toThrow();
});
});