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

View File

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

View File

@@ -64,7 +64,6 @@ describe('<ScheduleAdd />', () => {
interval: 10, interval: 10,
name: 'Run every 10 minutes 10 times', name: 'Run every 10 minutes 10 times',
occurrences: 10, occurrences: 10,
runOn: 'number',
startDateTime: '2020-03-25T10:30:00', startDateTime: '2020-03-25T10:30:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -85,7 +84,6 @@ describe('<ScheduleAdd />', () => {
frequency: 'hour', frequency: 'hour',
interval: 1, interval: 1,
name: 'Run every hour until date', name: 'Run every hour until date',
runOn: 'number',
startDateTime: '2020-03-25T10:45:00', startDateTime: '2020-03-25T10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -105,7 +103,6 @@ describe('<ScheduleAdd />', () => {
frequency: 'day', frequency: 'day',
interval: 1, interval: 1,
name: 'Run daily', name: 'Run daily',
runOn: 'number',
startDateTime: '2020-03-25T10:45:00', startDateTime: '2020-03-25T10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -127,7 +124,6 @@ describe('<ScheduleAdd />', () => {
interval: 1, interval: 1,
name: 'Run weekly on mon/wed/fri', name: 'Run weekly on mon/wed/fri',
occurrences: 1, occurrences: 1,
runOn: 'number',
startDateTime: '2020-03-25T10:45:00', startDateTime: '2020-03-25T10:45:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -148,7 +144,8 @@ describe('<ScheduleAdd />', () => {
interval: 1, interval: 1,
name: 'Run on the first day of the month', name: 'Run on the first day of the month',
occurrences: 1, occurrences: 1,
runOn: 'number', runOn: 'day',
runOnDayNumber: 1,
startDateTime: '2020-04-01T10:45', startDateTime: '2020-04-01T10:45',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -157,7 +154,7 @@ describe('<ScheduleAdd />', () => {
description: 'test description', description: 'test description',
name: 'Run on the first day of the month', name: 'Run on the first day of the month',
rrule: 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 () => { 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, interval: 1,
name: 'Run monthly on the last Tuesday', name: 'Run monthly on the last Tuesday',
occurrences: 1, occurrences: 1,
runOn: 'lastDay', runOn: 'the',
runOnTheDay: 'tuesday',
runOnTheOccurrence: -1,
startDateTime: '2020-03-31T11:00', startDateTime: '2020-03-31T11:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -179,7 +178,7 @@ describe('<ScheduleAdd />', () => {
description: 'test description', description: 'test description',
name: 'Run monthly on the last Tuesday', name: 'Run monthly on the last Tuesday',
rrule: 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 () => { test('Successfully creates a schedule with yearly repeat frequency on the first day of March', async () => {
@@ -191,7 +190,9 @@ describe('<ScheduleAdd />', () => {
interval: 1, interval: 1,
name: 'Yearly on the first day of March', name: 'Yearly on the first day of March',
occurrences: 1, occurrences: 1,
runOn: 'number', runOn: 'day',
runOnDayMonth: 3,
runOnDayNumber: 1,
startDateTime: '2020-03-01T00:00', startDateTime: '2020-03-01T00:00',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -200,7 +201,7 @@ describe('<ScheduleAdd />', () => {
description: 'test description', description: 'test description',
name: 'Yearly on the first day of March', name: 'Yearly on the first day of March',
rrule: 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 () => { test('Successfully creates a schedule with yearly repeat frequency on the second Friday in April', async () => {
@@ -212,7 +213,10 @@ describe('<ScheduleAdd />', () => {
interval: 1, interval: 1,
name: 'Yearly on the second Friday in April', name: 'Yearly on the second Friday in April',
occurrences: 1, occurrences: 1,
runOn: 'day', runOn: 'the',
runOnTheOccurrence: 2,
runOnTheDay: 'friday',
runOnTheMonth: 4,
startDateTime: '2020-04-10T11:15', startDateTime: '2020-04-10T11:15',
timezone: 'America/New_York', timezone: 'America/New_York',
}); });
@@ -221,7 +225,31 @@ describe('<ScheduleAdd />', () => {
description: 'test description', description: 'test description',
name: 'Yearly on the second Friday in April', name: 'Yearly on the second Friday in April',
rrule: 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 styled from 'styled-components';
import { useField } from 'formik'; import { useField } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
@@ -9,16 +9,25 @@ import {
Radio, Radio,
TextInput, TextInput,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import AnsibleSelect from '@components/AnsibleSelect';
import FormField from '@components/FormField'; import FormField from '@components/FormField';
import {
getDaysInMonth,
getDayString,
getMonthString,
getWeekString,
getWeekNumber,
} from '@util/dates';
import { required } from '@util/validators'; 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` const RunEveryLabel = styled.p`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -48,6 +57,21 @@ export function requiredPositiveInteger(i18n) {
} }
const FrequencyDetailSubform = ({ 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({ const [startDateTime] = useField({
name: 'startDateTime', name: 'startDateTime',
}); });
@@ -63,7 +87,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
name: 'interval', name: 'interval',
validate: requiredPositiveInteger(i18n), validate: requiredPositiveInteger(i18n),
}); });
const [runOn, runOnMeta, runOnHelpers] = useField({ const [runOn, runOnMeta] = useField({
name: 'runOn', name: 'runOn',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
}); });
@@ -79,20 +103,64 @@ const FrequencyDetailSubform = ({ i18n }) => {
validate: requiredPositiveInteger(i18n), validate: requiredPositiveInteger(i18n),
}); });
useEffect(() => { const monthOptions = [
// 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 key: 'january',
// happens then we'll clear out the selection and force the user value: 1,
// to choose between the remaining two. label: i18n._(t`January`),
if ( },
(frequency.value === 'month' || frequency.value === 'year') && {
runOn.value === 'lastDay' && key: 'february',
getDaysInMonth(startDateTime.value) - 7 >= value: 2,
new Date(startDateTime.value).getDate() label: i18n._(t`February`),
) { },
runOnHelpers.setValue(''); {
} key: 'march',
}, [startDateTime.value, frequency.value, runOn.value, runOnHelpers]); 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 updateDaysOfWeek = (day, checked) => {
const newDaysOfWeek = [...daysOfWeek.value]; 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 */ /* eslint-disable no-restricted-globals */
return ( return (
<> <>
@@ -252,7 +260,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('SU', checked); updateDaysOfWeek('SU', checked);
}} }}
aria-label={i18n._(t`Sunday`)} aria-label={i18n._(t`Sunday`)}
id="days-of-week-sun" id="schedule-days-of-week-sun"
name="daysOfWeek" name="daysOfWeek"
/> />
<Checkbox <Checkbox
@@ -262,7 +270,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('MO', checked); updateDaysOfWeek('MO', checked);
}} }}
aria-label={i18n._(t`Monday`)} aria-label={i18n._(t`Monday`)}
id="days-of-week-mon" id="schedule-days-of-week-mon"
name="daysOfWeek" name="daysOfWeek"
/> />
<Checkbox <Checkbox
@@ -272,7 +280,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('TU', checked); updateDaysOfWeek('TU', checked);
}} }}
aria-label={i18n._(t`Tuesday`)} aria-label={i18n._(t`Tuesday`)}
id="days-of-week-tue" id="schedule-days-of-week-tue"
name="daysOfWeek" name="daysOfWeek"
/> />
<Checkbox <Checkbox
@@ -282,7 +290,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('WE', checked); updateDaysOfWeek('WE', checked);
}} }}
aria-label={i18n._(t`Wednesday`)} aria-label={i18n._(t`Wednesday`)}
id="days-of-week-wed" id="schedule-days-of-week-wed"
name="daysOfWeek" name="daysOfWeek"
/> />
<Checkbox <Checkbox
@@ -292,7 +300,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('TH', checked); updateDaysOfWeek('TH', checked);
}} }}
aria-label={i18n._(t`Thursday`)} aria-label={i18n._(t`Thursday`)}
id="days-of-week-thu" id="schedule-days-of-week-thu"
name="daysOfWeek" name="daysOfWeek"
/> />
<Checkbox <Checkbox
@@ -302,7 +310,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('FR', checked); updateDaysOfWeek('FR', checked);
}} }}
aria-label={i18n._(t`Friday`)} aria-label={i18n._(t`Friday`)}
id="days-of-week-fri" id="schedule-days-of-week-fri"
name="daysOfWeek" name="daysOfWeek"
/> />
<Checkbox <Checkbox
@@ -312,7 +320,7 @@ const FrequencyDetailSubform = ({ i18n }) => {
updateDaysOfWeek('SA', checked); updateDaysOfWeek('SA', checked);
}} }}
aria-label={i18n._(t`Saturday`)} aria-label={i18n._(t`Saturday`)}
id="days-of-week-sat" id="schedule-days-of-week-sat"
name="daysOfWeek" name="daysOfWeek"
/> />
</div> </div>
@@ -328,21 +336,39 @@ const FrequencyDetailSubform = ({ i18n }) => {
isValid={!runOnMeta.touched || !runOnMeta.error} isValid={!runOnMeta.touched || !runOnMeta.error}
label={i18n._(t`Run on`)} label={i18n._(t`Run on`)}
> >
<Radio <RunOnRadio
id="run-on-number" id="schedule-run-on-day"
name="runOn" name="runOn"
label={generateRunOnNumberLabel()} label={
value="number" <div css="display: flex;align-items: center;">
isChecked={runOn.value === 'number'} {frequency?.value === 'month' && (
onChange={(value, event) => { <span id="foobar" css="margin-right: 10px;">
event.target.value = 'number'; Day
runOn.onChange(event); </span>
}} )}
/> {frequency?.value === 'year' && (
<Radio <AnsibleSelect
id="run-on-day" id="schedule-run-on-day-month"
name="runOn" css="margin-right: 10px"
label={generateRunOnDayLabel()} 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" value="day"
isChecked={runOn.value === 'day'} isChecked={runOn.value === 'day'}
onChange={(value, event) => { onChange={(value, event) => {
@@ -350,20 +376,105 @@ const FrequencyDetailSubform = ({ i18n }) => {
runOn.onChange(event); runOn.onChange(event);
}} }}
/> />
{new Date(startDateTime.value).getDate() > <RunOnRadio
getDaysInMonth(startDateTime.value) - 7 && ( id="schedule-run-on-the"
<Radio name="runOn"
id="run-on-last-day" label={
name="runOn" <div css="display: flex;align-items: center;">
label={generateRunOnLastDayLabel()} <span id="foobar" css="margin-right: 10px;">
value="lastDay" The
isChecked={runOn.value === 'lastDay'} </span>
onChange={(value, event) => { <AnsibleSelect
event.target.value = 'lastDay'; id="schedule-run-on-the-occurrence"
runOn.onChange(event); 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>
)} )}
<FormGroup <FormGroup

View File

@@ -172,14 +172,26 @@ function ScheduleForm({
interval: 1, interval: 1,
name: schedule.name || '', name: schedule.name || '',
occurrences: 1, occurrences: 1,
runOn: 'number', runOn: 'day',
runOnDayMonth: 1,
runOnDayNumber: 1,
runOnTheDay: 'sunday',
runOnTheMonth: 1,
runOnTheOccurrence: 1,
startDateTime: dateToInputDateTime(closestQuarterHour), startDateTime: dateToInputDateTime(closestQuarterHour),
timezone: schedule.timezone || 'America/New_York', timezone: schedule.timezone || 'America/New_York',
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validate={values => { validate={values => {
const errors = {}; const errors = {};
const { end, endDateTime, startDateTime } = values; const {
end,
endDateTime,
frequency,
runOn,
runOnDayNumber,
startDateTime,
} = values;
if ( if (
end === 'onDate' && 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; 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-never').prop('checked')).toBe(true);
expect(wrapper.find('input#end-after').prop('checked')).toBe(false); 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#end-on-date').prop('checked')).toBe(false);
}); expect(wrapper.find('input#schedule-run-on-day').prop('checked')).toBe(
test('month run on options displayed correctly as date changes', async () => { true
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#run-on-last-day').length).toBe(0); expect(
await act(async () => { wrapper.find('input#schedule-run-on-day-number').prop('value')
wrapper.find('input#schedule-start-datetime').simulate('change', { ).toBe(1);
target: { value: '2020-03-27T01:45:00', name: 'startDateTime' }, expect(wrapper.find('input#schedule-run-on-the').prop('checked')).toBe(
}); false
});
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#run-on-last-day').prop('checked')).toBe(false); expect(wrapper.find('select#schedule-run-on-day-month').length).toBe(0);
expect(wrapper.find('input#run-on-last-day + label').text()).toBe( expect(wrapper.find('select#schedule-run-on-the-month').length).toBe(0);
'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();
}); });
test('correct frequency details fields and values shown when frequency changed to year', async () => { test('correct frequency details fields and values shown when frequency changed to year', async () => {
const runFrequencySelect = wrapper.find( 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-never').prop('checked')).toBe(true);
expect(wrapper.find('input#end-after').prop('checked')).toBe(false); 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#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 () => { test('occurrences field properly shown when end after selection is made', 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 () => {
await act(async () => { await act(async () => {
wrapper.find('Radio#end-after').invoke('onChange')('after', { wrapper.find('Radio#end-after').invoke('onChange')('after', {
target: { name: 'end' }, target: { name: 'end' },
@@ -355,32 +281,6 @@ describe('<ScheduleForm />', () => {
}); });
wrapper.update(); 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 () => { 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-never').prop('checked')).toBe(true);
expect(wrapper.find('input#end-after').prop('checked')).toBe(false); expect(wrapper.find('input#end-after').prop('checked')).toBe(false);

View File

@@ -1,5 +1,6 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { RRule } from 'rrule';
import { getLanguage } from './language'; import { getLanguage } from './language';
const prependZeros = value => value.toString().padStart(2, 0); const prependZeros = value => value.toString().padStart(2, 0);
@@ -28,87 +29,37 @@ export function dateToInputDateTime(dateObj) {
return `${year}-${month}-${day}T${hour}:${minute}:${second}`; return `${year}-${month}-${day}T${hour}:${minute}:${second}`;
} }
export function getDaysInMonth(dateString) { export function getRRuleDayConstants(dayString, i18n) {
const dateObj = new Date(dateString); switch (dayString) {
return new Date(dateObj.getFullYear(), dateObj.getMonth() + 1, 0).getDate(); case 'sunday':
} return RRule.SU;
case 'monday':
export function getWeekNumber(dateString) { return RRule.MO;
const dateObj = new Date(dateString); case 'tuesday':
const dayOfMonth = dateObj.getDate(); return RRule.TU;
const dayOfWeek = dateObj.getDay(); case 'wednesday':
if (dayOfMonth < 8) { return RRule.WE;
return 1; case 'thursday':
} return RRule.TH;
dateObj.setDate(dayOfMonth - dayOfWeek + 1); case 'friday':
return Math.ceil(dayOfMonth / 7); return RRule.FR;
} case 'saturday':
return RRule.SA;
export function getDayString(dayIndex, i18n) { case 'day':
switch (dayIndex) { return [
case 0: RRule.MO,
return i18n._(t`Sunday`); RRule.TU,
case 1: RRule.WE,
return i18n._(t`Monday`); RRule.TH,
case 2: RRule.FR,
return i18n._(t`Tuesday`); RRule.SA,
case 3: RRule.SU,
return i18n._(t`Wednesday`); ];
case 4: case 'weekday':
return i18n._(t`Thursday`); return [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR];
case 5: case 'weekendDay':
return i18n._(t`Friday`); return [RRule.SA, RRule.SU];
case 6:
return i18n._(t`Saturday`);
default: default:
throw new Error(i18n._(t`Unrecognized day index`)); throw new Error(i18n._(t`Unrecognized day string`));
}
}
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`));
} }
} }

View File

@@ -1,12 +1,9 @@
import { RRule } from 'rrule';
import { import {
dateToInputDateTime, dateToInputDateTime,
getDaysInMonth,
getDayString,
getMonthString,
getWeekNumber,
getWeekString,
formatDateString, formatDateString,
formatDateStringUTC, formatDateStringUTC,
getRRuleDayConstants,
secondsToHHMMSS, secondsToHHMMSS,
} from './dates'; } from './dates';
@@ -59,63 +56,35 @@ describe('dateToInputDateTime', () => {
}); });
}); });
describe('getDaysInMonth', () => { describe('getRRuleDayConstants', () => {
test('it returns the expected value', () => { test('it returns the expected value', () => {
expect(getDaysInMonth('2020-02-15T00:00:00Z')).toEqual(29); expect(getRRuleDayConstants('monday', i18n)).toEqual(RRule.MO);
expect(getDaysInMonth('2020-03-15T00:00:00Z')).toEqual(31); expect(getRRuleDayConstants('tuesday', i18n)).toEqual(RRule.TU);
expect(getDaysInMonth('2020-04-15T00:00:00Z')).toEqual(30); 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);
describe('getWeekNumber', () => { expect(getRRuleDayConstants('sunday', i18n)).toEqual(RRule.SU);
test('it returns the expected value', () => { expect(getRRuleDayConstants('day', i18n)).toEqual([
expect(getWeekNumber('2020-02-01T00:00:00Z')).toEqual(1); RRule.MO,
expect(getWeekNumber('2020-02-08T00:00:00Z')).toEqual(2); RRule.TU,
expect(getWeekNumber('2020-02-15T00:00:00Z')).toEqual(3); RRule.WE,
expect(getWeekNumber('2020-02-22T00:00:00Z')).toEqual(4); RRule.TH,
expect(getWeekNumber('2020-02-29T00:00:00Z')).toEqual(5); RRule.FR,
}); RRule.SA,
}); RRule.SU,
]);
describe('getDayString', () => { expect(getRRuleDayConstants('weekday', i18n)).toEqual([
test('it returns the expected value', () => { RRule.MO,
expect(getDayString(0, i18n)).toEqual('Sunday'); RRule.TU,
expect(getDayString(1, i18n)).toEqual('Monday'); RRule.WE,
expect(getDayString(2, i18n)).toEqual('Tuesday'); RRule.TH,
expect(getDayString(3, i18n)).toEqual('Wednesday'); RRule.FR,
expect(getDayString(4, i18n)).toEqual('Thursday'); ]);
expect(getDayString(5, i18n)).toEqual('Friday'); expect(getRRuleDayConstants('weekendDay', i18n)).toEqual([
expect(getDayString(6, i18n)).toEqual('Saturday'); RRule.SA,
expect(() => getDayString(7, i18n)).toThrow(); RRule.SU,
}); ]);
}); expect(() => getRRuleDayConstants('foobar', 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();
}); });
}); });